Конспект курса "Аналитик данных"

Оглавление

Python

Урок№1

Горячие клавиши в ноутбуке

  • Выполнение ячейки

    Ctrl + Enter

    или

    Shift + Enter

  • Комментирование строки

    Ctrl + /

  • Создать ячейку выше

    Обратите внимание, эта команда и команды ниже будут работать, если у вас выделена ячейка (она подсвечена голубым, при этом курсор не мелькает внутри ячейки)

    a

  • Создать ячейку ниже

    b

  • Объединить выделенные ячейки

    Shift + m

  • Вырезать ячейки

    x

  • Копировать ячейки

    c

  • Вставить скопированные/вырезанные ячейки

    v

Больше информации

Арифметика

  • Сложение ```python 2 + 2 4 ```
  • Вычитание ```python -3 - 5 -8 ```
  • Умножение ```python 3 * 4 12 ```
  • Возведение в степень ```python 2 ** 3 8 ```
  • Деление ```python 10 / 2 5.0 ```
  • Деление нацело - в одиннадцати содержится пять целых двоек ```python 11 // 2 5 ```
  • Остаток от деления - при делении десяти нацело на два не остаётся остатка ```python 10 % 2 0 ```
  • Скобки как в математике меняют приоритет операций ```python (12 + 4) * 9 144 ```

Больше информации

Типы данных

Самые важные – числа и строки

Числа записываются как почти везде – просто последовательность цифр:
0, -5, 100

Дробные числа представляются как десятичные дроби, дробная часть обособляется точкой: 
10.5, 0.0, -300.5034

Строки заключены в кавычки – 'Ivan'. Кавычки могут быть одинарными, двойными, или их утроенной версией: 
'', "", '''''', """"""

Больше информации

Переменные

variable = 3

Слева – название переменной, затем следует знак равно, а справа – значение, которое было присвоено переменной.

Нужны для хранения значений, при вычислении вместо переменной подставляется её значение.

Больше информации

Сравнения

Числа можно сравнивать, в результате сравнений получается логическое значение – True или False

  • Больше ```python 5 > 10 False ```
  • Меньше ```python 3 < 6 True ```
  • Больше либо равно ```python 4 >=4 True ```
  • Меньше либо равно ```python 10 <= 9 False ```
  • Равно – используется 2 знака равенства, потому что 1 знак является оператором присвоения для создания переменных ```python 5 == 5 True ```
  • Не равно ```python 6 != 3 True ```

Больше информации

Операции над строками

Строки можно складывать, получая их соединение (конкатенат)

'Vasya' + 'Petrov'
'VasyaPetrov'

Больше информации

Списки (листы, массивы или эрреи)

Более сложный тип данных, позволяющий хранить много значений различных типов. Для его создания необходимо перечислить внутри квадратных скобок [] значения через запятую:

In [2]:
employees = ['Anatoly', 'Alexander', 'Lavrentii']
employees
Out[2]:
['Anatoly', 'Alexander', 'Lavrentii']

Список можно изменять (например, добавлять новые значения):

In [3]:
employees.append('Rostislav') 
employees
Out[3]:
['Anatoly', 'Alexander', 'Lavrentii', 'Rostislav']

В данном случае мы использовали метод .append(), добавляющий значение в список, у которого мы задействовали метод

Методы похожи на функции, только вызываются у конкретного объекта (в данном случае списка).

Индексирование

Значения (элементы) из списка можно достать с помощью индексирования – для этого укажите после списка в квадратных скобках номер нужного элемента, начиная с 0. Помимо этого можно индексироваться с конца списка, используя отрицательные числа:

In [4]:
employees[0]
Out[4]:
'Anatoly'
In [5]:
employees[2]
Out[5]:
'Lavrentii'
In [6]:
employees[-1] # negative to index from the end
Out[6]:
'Rostislav'

Также можно брать срезы (слайсы) списка. То есть получать кусочек исходного списка:

  • От элемента с индексом 1 до конца
In [8]:
employees[1:]
Out[8]:
['Alexander', 'Lavrentii', 'Rostislav']
  • От начала до элемента с индексом 2
In [9]:
employees[:2]
Out[9]:
['Anatoly', 'Alexander']
  • От начала до конца с шагом 2
In [10]:
employees[::2]
Out[10]:
['Anatoly', 'Lavrentii']

Синтаксис среза такой

[start : stop : step]

  • start – от какого элемента берём значения, по умолчанию равно 0
  • stop – до какого элемента берём значения, не включая его, по умолчанию равно длине списка
  • step – шаг, с которым берём элементы, по умолчанию равен 1

Дефолтные аргументы можно пропускать

Метод pop()

Чтобы убрать элемент из списка по его порядковому номеру (индексу) используется метод .pop()

In [11]:
employees.pop(1)
Out[11]:
'Alexander'

При этом убираемый из списка элемент возвращается пользователю (мы можем его использовать):

In [12]:
lecturer_name = employees.pop(0)
employees
Out[12]:
['Lavrentii', 'Rostislav']
In [13]:
lecturer_name
Out[13]:
'Anatoly'

Правила оформления кода

Конвенциональны и не влияют на работу программы. Их соблюдают для единого стиля программ от разных программистов. Часть из них:

  • операнды и операторы (значения и знаки операций, например + или *) разделяются пробелами
  • перед запятой не ставится пробел, но ставится после
  • переменные следует называть осмысленно – код становится проще читать, он становится осмысленнее (это может показаться незначительным, но одна из самых важных вещей)

Документация

Комментарии

Используются для объяснения того, что происходит в коде. Очень важны, так как код чаще читают, чем пишут, и комментарии упрощают понимание происходящего.

Начинаются с решётки, дальнейшая строка игнорируется питоном

# эта строка нужна для объяснения, она не влияет на скрипт

Больше информации

Использованные функции

  • len() – находит длину коллекции (это группа типов данных, куда входят строки, списки, словари)
    len([10, 20, 35])
    3
    

Больше информации

  • print() – печатает то, что указано внутри скобок

Больше информации

  • str() – превращает переданное значение в строку
    str(5)
    '5'
    

Больше информации

  • int() – превращает переданное значение в целое число
    int('10')
    10
    

Больше информации

  • float() – превращает переданное значение в дробное число
    float('2.5')
    2.5
    

Больше информации

Условия

Конструкция для управления ходом программы (что делать в каких-то случаях).

if predicate:
    what to do
  • if – ключевое слово, которое даёт понять питону что дальше будет условие (ветвь)

  • predicate – какое-то выражение, которое сводится к логическому (True/False)

  • : – элемент синтаксиса (просто так нужно, чтобы питон понял)

  • отступ – 4 пробела или tab (используйте в программе что-то одно из этих вариантов), используется для обозначения на каком уровне мы находимся, улучшает читаемость

  • what to do – команды, которые вы хотите выполнить, если попали в эту ветку (то есть predicate True); все строчки в одном условии должны быть с этим уровнем отступа, чтобы выполняться в нём

Помимо простого условия, которое или выполнится или нет, есть более сложные – с 2-мя и более ветвями. Для их обозначения используются ключевые слова elif и else. Их можно использовать только если условие началось (вплотную до этого была ветка if или elif)

elif – ключевое слово, означающее, что дальше идёт условие, которое будет выполняться только если все предыдущие условия не выполнились (predicate в них был False) и предикат этой ветки True

Для использования elif обязательно должна быть ветка if выше

else - ключевое слово, означающее, что дальше идёт условие, которое будет выполняться в том случае, если все предыдущие условия не выполнились

Для использования else обязательно должна быть ветка if или elif выше

In [16]:
if 10 > 0:
    print('1st branch was executed')
elif 10 < 0:
    print('This branch is False')
else:
    print('This branch is unreachable in this program')
1st branch was executed

Логические операторы

Для логических значений True и False есть свои специальные операторы (как + или * для чисел). Они бывают полезны при задании предикатов в условиях и для анализа таблиц, где есть логический тип. Например, таблица с клиентами, где есть колонка просрочил ли клиент выплаты по кредитам.

Итак, что же это за операторы?

  • not – инвертирует логическое значение
In [17]:
not True 
Out[17]:
False
In [18]:
not False
Out[18]:
True
  • or – даёт True, если хотя бы один из операндов True
In [19]:
False or True
Out[19]:
True
In [20]:
False or False
Out[20]:
False
  • and – даёт False, если хотя бы один из операндов False
In [24]:
False and True
Out[24]:
False
In [25]:
True and True
Out[25]:
True

Логические действия можно соединять друг с другом и указывать порядок выполнения операций, прямо как с арифметическими действиями!

In [26]:
True and (False or True)  # True and True
Out[26]:
True

Циклы (петли)

Конструкция для перебора значений из списка

for something in collection: what to do

  • for – ключевое слово, которое даёт понять питону, что дальше будет цикл

  • something – переменная, куда будет последовательно подставляться значение из списка; нужна, чтобы обращаться к значению

  • in – ключевое слово, показывающее, что мы работаем с элементом коллекции

  • : – как в условии

  • отступ – как в условии

  • what to do – тело цикла; то, что будет происходить для каждого из элементов

In [27]:
for x in [1, 2, 3]:
    print(x + 10)
11
12
13

Вложенные конструкции

Циклы и условия можно комбинировать и получать более сложные программы. Отступы отображают питону и программисту какая строка к какой конструкции относится.

In [28]:
# Create list with some data
rates = [1.3, 0.95, 1.2, 1.1, 0.7, 1.35]


# Here comes the cycle
for rate in rates:
    if rate > 1:  # Start condition, 1 indent for cycle, 1 indent for condition
        print('rate is greater than 1')  # body of condition, 1 indent for cycle, 1 indent for condition
    else:
        print('rate is lesser than 1')
rate is greater than 1
rate is lesser than 1
rate is greater than 1
rate is greater than 1
rate is lesser than 1
rate is greater than 1

Форматирование строк

Очень полезно иметь матрицу (template) строки, в которую можно подставлять произвольную подстроку.

In [27]:
intro = "My name is {}, I'm from {}"
intro.format('Sasha', 'Russia')
Out[27]:
"My name is Sasha, I'm from Russia"
In [28]:
intro.format('Katya', 'Russia')
Out[28]:
"My name is Katya, I'm from Russia"

{} внутри строки означают возможность применения метода format. Переданные в него аргументы будут подставлены в соответствующие скобки.

Существуют и другие варианты темплэйтов
Больше информации

Словари (дикты)

Ассоциативный тип данных – в нём каждый элемент является парой ключ-значение. Для создания нужно указать элементы внутри фигурных скобок – {}.

Синтаксис элементов в словаре – {key: value}.

In [29]:
salaries = {'Ivan': 30000}  # key 'Ivan' is associated with value 30000

Чтобы узнать что ассоциировано с ключом 'Ivan', нужно проиндексироваться по нему:

In [30]:
salaries['Ivan']
Out[30]:
30000

Ключами словаря могут быть строки и числа, а значениями почти что угодно – числа, строки, списки, даже другие словари!

Задавание нового элемента в словаре

In [31]:
salaries['Anna'] = 50000

salaries
Out[31]:
{'Ivan': 30000, 'Anna': 50000}

Итерирование по словарю

  • по ключам
In [32]:
for name in salaries: # salaries.keys() is analogous 
    print(name) 
Ivan
Anna
  • по значениям
In [33]:
for salary in salaries.values():
    print(salary)
30000
50000

Урок№2

Библиотека (модуль/либа/пакет)

Библиотека – набор кода, посвящённый определённой цели (математические функции/работа в интернете/обработка текстов). Используется, чтобы облегчить работу, связанную с этой целью. В нашем курсе самый простой пример – библиотека pandas, использующаяся для анализа данных.

Если есть библиотека, рещающая ваша задачу, то почти всегда лучше воспользоваться ей, нежели решать всё смостоятельно с нуля.

По умолчанию содержимое библиотек недоступно при работе в python. Чтобы их использовать, нужно произвести импорт:

import pandas as pd
  • import – ключевое слово, дающее питону понять, что будут импортировать библиотеку

  • pandas – название библиотеки

  • as – ключевое слово, означающее, что дальше будет использоваться другое название (элиас, alias)

  • pd – то, какое название мы решили использовать в коде вместо названия этой библиотеки; они произвольны и обычно конвенциональны (в дальнейшем будем использовать название pd)

Более простой вариант импорта без изменения названия

import module

module – название нужного вам модуля, далее в коде используется оно.

Существуют и другие способы импорта, но пока что остановимся на этих.

pandas

Библиотека для работы с данными. Без pandas анализ табличных данных в питоне совсем не то.

В следующих стэпах поговорим о его возможностяx!

Документация

Считывание csv

pd.read_csv('path_to_your.csv')  # read_excel for reading excel files

Считывает csv файл, который лежит по указанному в скобках пути. На windows пути к файлам содержат \, который является специальным символом в строках во многих языках программирования, включая питон. Поэтому необходимо сделать следующее - либо удвоить все \ в строке с путём, либо поставить r перед строкой.

путь на windows - C:\user\docs\Letter.txt

cтрока с путём - 'C:\user\docs\Letter.txt'

валидная строка с путём - 'C:\\user\\docs\\Letter.txt' или r'C:\user\docs\Letter.txt

На серевере мы работаем с юниксовыми путями, например /home/user/letter.txt. С ними не возникает таких проблем – достаточно поместить путь в кавычки, чтобы всё было хорошо.

Дополнительные аргументы функции read_csv

Аргументы (или параметры) – это настройки, которые мы можем задать в функции:

  • encoding – параметр в read_csv, отвечает за кодировку текста, которая может быть различной. Самая распространённая – utf-8. Пример указания кодировки:
pd.read_csv('path_to_your.csv', encoding='Windows-1251')  # now you are reading file encoded with Windows-1251
  • sep – разделитель между ячейками в строке (по умолчанию ,)
pd.read_csv('path_to_your.csv', encoding='Windows-1251', sep=';')  # now you additionally specified that fields are separated with ;
  • parse_dates – стоит ли воспринимать даты как даты? (по умолчанию воспринимаются пандасом как строки)
    Параметр с датами может принимать несколько значений:
    • True – пытается перевести в дату первую колонку
    • список колонок – пытается перевести в дату указанные колонки
# And create_data, payment_data columns will be treated as data
pd.read_csv('path_to_your.csv', encoding='Windows-1251', sep=';', parse_dates=['create_data', 'payment_data'])

Документация

In [30]:
import pandas as pd
# Paste here appropriate link from your course
df = pd.read_csv('https://stepik.org/media/attachments/2_taxi_nyc.csv')
df.head()
Out[30]:
pickup_dt pickup_month borough pickups hday spd vsb temp dewp slp pcp 01 pcp 06 pcp 24 sd
0 2015-01-01 01:00:00 Jan Bronx 152 Y 5.0 10.0 30.0 7.0 1023.5 0.0 0.0 0.0 0.0
1 2015-01-01 01:00:00 Jan Brooklyn 1519 Y 5.0 10.0 30.0 7.0 1023.5 0.0 0.0 0.0 0.0
2 2015-01-01 01:00:00 Jan EWR 0 Y 5.0 10.0 30.0 7.0 1023.5 0.0 0.0 0.0 0.0
3 2015-01-01 01:00:00 Jan Manhattan 5258 Y 5.0 10.0 30.0 7.0 1023.5 0.0 0.0 0.0 0.0
4 2015-01-01 01:00:00 Jan Queens 405 Y 5.0 10.0 30.0 7.0 1023.5 0.0 0.0 0.0 0.0

Размеры датафрэйма

shape – атрибут, хранящий данные о размерах таблицы. Возвращает кортеж (для простоты – смотрите на него как на неизменяемый список). В случае датафрэйма кортеж содержит 2 значения – число строк и число колонок в нём.

df.shape

Например, в данном случае размер таблицы: 29101 строк и 14 колонки

Документация

Типы колонок

Чтобы узнать типы колонок в вашем датафрэйме, воспользуйтесь атрибутом dtypes – он возвращает серию с описанием типа каждой колонки. Типы более-менее совпадают с типами в python, кроме нескольких различий:

  • здесь у типов присутствует описание размера (числа битов)
  • все сложные типы (не числа или логические значения) отображаются как object

Информация о типе важна для дальнейшей работы с датафрэймом (например, чтобы не произвести сложение строк, думая, что это числа).

Документация

describe

describe – удобный метод для вывода описания числовых колонок в датафрэйме

In [118]:
df.describe()
Out[118]:
pickups spd vsb temp dewp slp pcp 01 pcp 06 pcp 24 sd
count 29101.000000 29101.000000 29101.000000 29101.000000 29101.000000 29101.000000 29101.000000 29101.000000 29101.000000 29101.000000
mean 490.215903 5.984924 8.818125 47.669042 30.823065 1017.817938 0.003830 0.026129 0.090464 2.529169
std 995.649536 3.699007 2.442897 19.814969 21.283444 7.768796 0.018933 0.093125 0.219402 4.520325
min 0.000000 0.000000 0.000000 2.000000 -16.000000 991.400000 0.000000 0.000000 0.000000 0.000000
25% 1.000000 3.000000 9.100000 32.000000 14.000000 1012.500000 0.000000 0.000000 0.000000 0.000000
50% 54.000000 6.000000 10.000000 46.000000 30.000000 1018.200000 0.000000 0.000000 0.000000 0.000000
75% 449.000000 8.000000 10.000000 64.500000 50.000000 1022.900000 0.000000 0.000000 0.050000 2.958333
max 7883.000000 21.000000 10.000000 89.000000 73.000000 1043.400000 0.280000 1.240000 2.100000 19.000000

describe выводит информацию о числе строк, среднем, стандартном отклонении, минимуме, максимуме и значениях по 25-ому, 50-ому и 75-ому квартилям. Он действует только на числах, так как большинство этих параметров неочевидно определяются для других типов данных (например, строк)

Документация

Переименование

В идеале названия колонок осмыслены, актуальны, не содержат пробелов и на английском. Конечно, для каких-то задач, они могут быть и с пробелами, и на другом языке. В любом случае, если вы хотите их переименовать есть метод rename. Пример переименования колонки x в name, а колонки y в salary:

In [119]:
# Rename columns
df = df.rename(columns={'x': 'name', 'y': 'salary'})

А это пример переименовывания лэйблов строк из 0 в Ivanov и из 1 в Vasilev:

In [120]:
# Rename index (row names)
df = df.rename(index={0: 'Ivanov', 1: 'Vasilev'})

Один из способов переименования – передать словарь, где ключи это старые названия, а значения – новые.

Документация

Обращение к колонкам

В пандасе существует много способов обратиться к колонке датафрэйма. Самый удобный:
df.column_name

df – датафрэйм
column_name – название колонки

Чтобы это работало, название колонки должно состоять из одного компонента (например, слова), и не должно совпадать с названием методов датафрэйма (имя колонки count не сработает). Для языковой однородности – ещё и на английском, но это не является обязательным.

Что делать, если название колонки состоит из 2-х слов? Либо переименовать, либо использовать другой способ доступа:

df['column name']

Работает для всех случаев кроме тех, когда в названии присутствуют одинарные кавычки. Тогда либо используйте вокруг названия двойные, либо поставьте \ перед кавычками внутри. Лучше называть колонки без кавычек.

Для получения нескольких колонок передайте внутрь квадратных скобок список с именами желаемых колонок:

df[['column1', 'column2', 'column3']]

Документация

Создание колонки

Так же просто как задавание нового значения в словаре :)

In [121]:
# Create new column in the df with name new_column which is equal to 5 in each cell
df['new_column'] = 5

Применение вычислительных методов

Существует набор методов, доступных для колонок датафрэймов. Например, есть колонка money в датафрэйме, содержащая полученные объёмы денег. Применив метод sum, можно посчитать их сумму.

In [32]:
df.pickups.sum()
Out[32]:
14265773

Ещё несколько примеров:

product – перемножение
std – среднеквадратичное отклонение
var – дисперсия

Больше информации

Цепочка методов (method chaining)

Приём для объединения нескольких действий в одно. Большинство методов датафрэймов возвращают вам результат, который довольно часто тоже является датафрэймом. Следовательно, от него тоже можно вызвать метод.

Ванильная запись:

In [162]:
df = pd.read_csv('https://stepik.org/media/attachments/2_taxi_nyc.csv')
df = df.query('pickups >=  1000')
df = df.groupby(['pickup_month', 'borough'], as_index=False).agg({'pickups': 'sum'})  # groupby is usually immediately followed by agg
df = df.sort_values(['pickups'])
df.head()
Out[162]:
pickup_month borough pickups
8 Mar Brooklyn 30566
4 Jan Brooklyn 36570
2 Feb Brooklyn 42427
0 Apr Brooklyn 57653
6 Jun Brooklyn 115993

Сокращённая запись:

In [163]:
df = pd.read_csv('https://stepik.org/media/attachments/2_taxi_nyc.csv')
df  = df.query('pickups >=  1000').groupby(['pickup_month', 'borough'], as_index=False).agg({'pickups': 'sum'}).sort_values(['pickups'])
df.head()
Out[163]:
pickup_month borough pickups
8 Mar Brooklyn 30566
4 Jan Brooklyn 36570
2 Feb Brooklyn 42427
0 Apr Brooklyn 57653
6 Jun Brooklyn 115993

Как вы видите, эта запись довольно длинная, и не очень удобна для чтения. Обычно, такая цепочка оформляется в блок, где каждый метод идёт на своей строке. Есть 2 варианта оформления, какой выбрать - вопрос предпочтения и конвенций в вашей организации:

In [126]:
df = pd.read_csv('https://stepik.org/media/attachments/2_taxi_nyc.csv')
# \ after each nonfinal line to demarcate line continuation for python
df  = df.query('pickups >=  1000') \
        .groupby(['pickup_month', 'borough'], as_index=False) \
        .agg({'pickups': 'sum'}) \
        .sort_values(['pickups'])
df.head()
Out[126]:
pickup_month borough pickups
8 Mar Brooklyn 30566
4 Jan Brooklyn 36570
2 Feb Brooklyn 42427
0 Apr Brooklyn 57653
6 Jun Brooklyn 115993
In [127]:
df = pd.read_csv('https://stepik.org/media/attachments/2_taxi_nyc.csv')
# Parentheses around the whole expression for the same purpose as backslash in previous example

df  = (df.query('pickups >=  1000')
         .groupby(['pickup_month', 'borough'], as_index=False)
         .agg({'pickups': 'sum'})
         .sort_values(['pickups']))
df.head()
Out[127]:
pickup_month borough pickups
8 Mar Brooklyn 30566
4 Jan Brooklyn 36570
2 Feb Brooklyn 42427
0 Apr Brooklyn 57653
6 Jun Brooklyn 115993

Группировка

Часто используемый приём для вычисления чего-либо по данным. Осуществляется с помощью метода groupby – группирует данные в датафрэйме по указанным колонкам:

Применение одного метода groupby не даёт видимого эффекта, хотя на самом деле все строки были объединены в группы по значению в колонке company: с одним значением в одну группу, с другим – в другую. groupby обычно используется не сам по себе, а в связке с agg или другим методом. Можно использовать несколько колонок для группировки, передав их в виде списка.

Дополнительные параметры:

  • as_index – принимает True или False для обозначения того, нужно ли использовать переданные для группировки колонки как индекс позднее, по умолчанию True

Документация

Агрегация

agg – функция для агрегирования данных, применяется после группировки groupby'ем.

Как это работает:
агрегируем методом agg, указывая на каких колонках какие действия производить

In [128]:
df = pd.read_csv('https://stepik.org/media/attachments/2_taxi_nyc.csv')
df.groupby('borough').agg({'pickups':'sum'})
Out[128]:
pickups
borough
Bronx 220047
Brooklyn 2321035
EWR 105
Manhattan 10367841
Queens 1343528
Staten Island 6957

Существуют разные способы передать в agg что и как вы хотите агрегировать. Самый простой и полный – использовать словарь, где ключи это названия колонок, а значения – применяемые к ним функции. Чтобы применить несколько функций, используйте список функций. Функции могут быть переданы сами (sum), либо строки, их обозначающие ('sum').

В результате агрегации из массива значений (колонка) получается одно значение на каждую агрегирующую функцию.

Документация

Сортировка значений

Для такой сортировки используется метод sort_values, получающий колонку/список колонок, по которым будет идти сортировка (обратите внимание, заглавные буквы считаются меньше обычныx):

Дополнительные параметры:

ascending – принимает логическое значение, показывающее сортировать ли колонку по возрастанию

In [130]:
df.sort_values('borough').head()
Out[130]:
pickup_dt pickup_month borough pickups hday spd vsb temp dewp slp pcp 01 pcp 06 pcp 24 sd
0 2015-01-01 01:00:00 Jan Bronx 152 Y 5.0 10.0 30.0 7.0 1023.5 0.000 0.0 0.000 0.0
20290 2015-05-08 02:00:00 May Bronx 20 N 0.0 10.0 61.0 49.0 1021.0 0.000 0.0 0.000 0.0
6405 2015-02-10 11:00:00 Feb Bronx 31 N 10.0 6.5 26.0 18.0 1017.0 0.005 0.0 0.005 8.0
20297 2015-05-08 03:00:00 May Bronx 19 N 3.0 10.0 59.0 50.0 1021.1 0.000 0.0 0.000 0.0
6398 2015-02-10 10:00:00 Feb Bronx 32 N 9.5 10.0 26.0 18.5 1016.4 0.000 0.0 0.010 8.0

value_counts

Метод, который считает, сколько раз встречается каждое уникальное значение переменной. Например, имеется следующий набор данных:

Посчитать, сколько раз встречается каждый месяц (pickup_month), можно с помощью следующей команды:

In [131]:
df['pickup_month'].value_counts()
Out[131]:
May    5058
Mar    4957
Jun    4899
Jan    4897
Apr    4798
Feb    4492
Name: pickup_month, dtype: int64

Результат возвращается в формате pd.Series (серии)

Также метод value_counts принимает на вход несколько параметров:

  • normalize – показать относительные частоты уникальных значений (по умолчанию равен False).
  • dropna – не включать количество NaN (по умолчанию равен True)
  • bins – сгруппировать количественную переменную (например, разбить возраст на возрастные группы); для использования данного параметра нужно указать, на сколько групп разбить переменную

Несколько примеров:

  1. Получаем частоту встречаемости (напр. pickup_month – в 40% наблюдений), также не удаляем из результата NaN:
In [132]:
df['pickup_month'].value_counts(normalize=True, dropna=False)
Out[132]:
May    0.173808
Mar    0.170338
Jun    0.168345
Jan    0.168276
Apr    0.164874
Feb    0.154359
Name: pickup_month, dtype: float64
  1. Разбиваем pickups на 2 промежутка:
In [133]:
df.pickups.value_counts(bins=2)
Out[133]:
(-7.884, 3941.5]    28398
(3941.5, 7883.0]      703
Name: pickups, dtype: int64

Запросы

В пандасе есть возможность фильтровать данные используя SQL-like синтаксис. Для этого нужен метод query, принимающий строку с запросом. Внутри него можно использовать названия колонок (если они без пробелов). При использовании строк внутри запроса экранируйте кавычки \ или используйте другую пару

В query также можно передать сразу несколько условий. Условия, которые должны выполняться одновременно, соединяются с помощью and или &:

In [33]:
df.query('pickup_month == "Jan" and borough == "Bronx"').head()
Out[33]:
pickup_dt pickup_month borough pickups hday spd vsb temp dewp slp pcp 01 pcp 06 pcp 24 sd
0 2015-01-01 01:00:00 Jan Bronx 152 Y 5.0 10.0 30.0 7.0 1023.5 0.0 0.0 0.0 0.0
7 2015-01-01 02:00:00 Jan Bronx 120 Y 3.0 10.0 30.0 6.0 1023.0 0.0 0.0 0.0 0.0
14 2015-01-01 03:00:00 Jan Bronx 132 Y 5.0 10.0 30.0 8.0 1022.3 0.0 0.0 0.0 0.0
21 2015-01-01 04:00:00 Jan Bronx 128 Y 5.0 10.0 29.0 9.0 1022.0 0.0 0.0 0.0 0.0
28 2015-01-01 05:00:00 Jan Bronx 87 Y 5.0 10.0 28.0 9.0 1021.8 0.0 0.0 0.0 0.0

Когда должно удовлетворяться одно из условий – or или |:

In [135]:
df.query('pickup_month == "Jan" or borough == "Bronx"').head()
Out[135]:
pickup_dt pickup_month borough pickups hday spd vsb temp dewp slp pcp 01 pcp 06 pcp 24 sd
0 2015-01-01 01:00:00 Jan Bronx 152 Y 5.0 10.0 30.0 7.0 1023.5 0.0 0.0 0.0 0.0
1 2015-01-01 01:00:00 Jan Brooklyn 1519 Y 5.0 10.0 30.0 7.0 1023.5 0.0 0.0 0.0 0.0
2 2015-01-01 01:00:00 Jan EWR 0 Y 5.0 10.0 30.0 7.0 1023.5 0.0 0.0 0.0 0.0
3 2015-01-01 01:00:00 Jan Manhattan 5258 Y 5.0 10.0 30.0 7.0 1023.5 0.0 0.0 0.0 0.0
4 2015-01-01 01:00:00 Jan Queens 405 Y 5.0 10.0 30.0 7.0 1023.5 0.0 0.0 0.0 0.0

Запись в файл

Датафрэйм можно записать в различный формат, самый распространённый, пожалуй, csv. Для этого нужно применить к датафрэйму метод to_csv и передать в него путь, по которому вы хотите создать файл.

df.to_csv('my.csv')

Дополнительные параметры:

  • index – записать индекс датафрэйма в csv как первую колонку
  • sep – используемый при записи разделитель колонок

Документация

Использованные функции

In [136]:
my_string = 'I_love_whitespaces'
my_string.replace('_',' ') 
Out[136]:
'I love whitespaces'
  • strip – применяется к строкам, по умолчанию убирает пробелы слева и справа
    Больше информации
In [137]:
user = '        Vasya Fedorov  '
user.strip()
Out[137]:
'Vasya Fedorov'
  • round – применяется к дробным числам, округляет их, можно передать дополнительный аргумент, который означает число знаков после запятой
    Больше информации
In [138]:
round(3.14159265359 ,2)
Out[138]:
3.14

Векторизация

Векторизация - это специальная техника, позволяющая в пандасе быстро выполнять в одну строчку операции, которые в чистом питоне требуют как минимум одного цикла. Быстрота связана с тем, что код пандаса реализован на более быстром чем питон языке, а в питоне просто представлены функции. Благодаря векторизации, мы можем делать различные операции со всеми колонками целиком, не отвлекаясь на итерирование по элементам

In [144]:
df.spd.head()
Out[144]:
0    5.0
1    5.0
2    5.0
3    5.0
4    5.0
Name: spd, dtype: float64

Умножим каждый элемент на 3

In [143]:
(df.spd * 3).head()
Out[143]:
0    15.0
1    15.0
2    15.0
3    15.0
4    15.0
Name: spd, dtype: float64

Или выясним для каждого значения больше ли оно чем 5

In [145]:
(df.spd > 5).head()
Out[145]:
0    False
1    False
2    False
3    False
4    False
Name: spd, dtype: bool

None

None это специальный тип данных в питоне, который имеет только одно одноимённое значение – None. Используется в тех случаях, когда нужно обозначить ничто. Обычно (но совсем необязательно) его возвращают функции, которые как-то изменяют данные

In [34]:
xs = [1, 2, 3]
a = xs.append(4)
In [35]:
print(xs)
[1, 2, 3, 4]
In [36]:
print(a)
None

Толку от присвоения выше a = xs.append(4) нет, просто постепенно запоминайте такие функции, и не перезаписывайте ваши данные вызовами типа

In [149]:
xs = xs.append(4) # так делать не нужно

Свои функции

Служат для переиспользования кода – вы написали изумительный скрипт, который решает 20% вашей работы. Теперь вам нужно использовать его каждый день на новых данных. Чтобы это упростить, существуют функции. Они позволяют свести весь код к 1-ой строке и менять название поступающих файлов и других параметров только в 1-ом месте. Задавание функций:

def function(parameter):
    # what to do
  • def – ключевое слово, даёт питону понять, что дальше будет задана функция
  • function – название, которое мы выбрали
  • parameter – инпут функции, то, какие данные ей нужны для работы. Параметров может быть 0 (без инпута) или больше. Это может быть почти что угодно:
    • путь к файлу
    • название папки
    • число строк, с которыми нужно работать
    • путь к отчёту, который будет создан
  • what to do – тело функции, функционал, который будет работать

По умолчанию функция при завершении своей работы ничего не вернёт обратно (в отличие от, например, метода head у датафрэйма). Чтобы она возвращала значение после вызова, добавьте ключевое слово return в теле функции перед переменной, которую хотите вернуть:

In [37]:
## 2 simple functions

# Without return
def print_a(a, b):
    print(a)

# With return
def return_a(a, b):
    return a
In [40]:
x = print_a(3, 5)  # x is None
print(x)
3
None
In [41]:
y = return_a(3, 5)  # y is 3
print(y)
3

Время

Для работы с датой и временем можно использовать модуль datetime. Для получения данных о времени в момент вызова функции используйте функцию today в одноимённом подмодуле:

In [153]:
import datetime  # by convention imports are placed in the head of file and separated with 2 blank lines from other code

date = datetime.datetime.today()
date
Out[153]:
datetime.datetime(2020, 7, 10, 14, 29, 31, 995327)

Само по себе это даст вам специальный тип даты. Чтобы перевести его в строку сделайте следующее:

strftime форматирует дату по переданному ему формату:

  • % – обозначает что дальше будет часть даты
  • Y – год 4-мя знаками
  • m – месяц 2-мя знаками
  • d – день
  • H – час
  • M – минуты
  • S – секунды

Можно использовать только часть фрагментов даты, разделители между ними – на ваше усмотрение (в примере это / и :). Немного примеров

In [154]:
from datetime import datetime

# current date and time
now = datetime.now() 
print(f'Full time format of now is {now}')
Full time format of now is 2020-07-10 14:29:39.431739
In [155]:
# Year
year = now.strftime("%Y")
print("year:", year)
year: 2020
In [156]:
# Month
month = now.strftime("%m")
print("month:", month)
month: 07
In [157]:
# Day
day = now.strftime("%d")
print("day:", day)
day: 10
In [158]:
# Time
time = now.strftime("%H:%M:%S")
print("time:", time)
time: 14:29:39
In [159]:
# Date and time
date_time = now.strftime("%m/%d/%Y, %H:%M:%S")
print("date and time:",date_time)
date and time: 07/10/2020, 14:29:39

Погрешность арифметики

В компьютере используется двоичная система счисления, в которой не выразить точно любое десятичное число. Из-за этого при выполнении действий может накапливаться ошибка. Обычно это не страшно (она в порядках меньше -5-ого), но бывает нужна точность. Для этого есть специальные библиотеки

Документация

Урок№3

Уникальные значения

unique – метод, возвращающий уникальные значения в колонке.

Уникальные значения возвращаются в форме array – о них будет сказано попозже, для простоты воспринимайте их как списки.

In [160]:
df.borough.unique()
Out[160]:
array(['Bronx', 'Brooklyn', 'EWR', 'Manhattan', 'Queens', 'Staten Island',
       nan], dtype=object)

Число уникальных значений

nunique – метод, который считает число уникальных значений в колонке.

In [161]:
df.borough.nunique()
Out[161]:
6

Медиана

Чтобы посчитать медиану колонки, используйте метод median

In [162]:
df.pickups.median()
Out[162]:
54.0

Среднее

Ну а для среднего – mean

In [163]:
df.pickups.mean()
Out[163]:
490.2159032335659

split

split – метод, разбивающий строку на куски и помещающий фрагменты в список. По умолчанию делит по пустым символам (пробел, табы, перенос строки).

In [164]:
brand_info = 'MARAVILLA 500 G Store_Brand'
brand_info.split()
Out[164]:
['MARAVILLA', '500', 'G', 'Store_Brand']

Анонимные функции

Обычно используются, когда нужно куда-то быстро поместить нечасто используемый функционал. Если вы планируете использовать анонимную функцию больше одного раза, напишите обычную функцию :)

lambda x: do something

  • labmda – ключевое слово, задающее анонимную функцию (не имеющую имени)
  • x – то, как мы назвали аргумент, принимаемый функцией
  • : – разделяет заголовок и тело безымянной функции
  • do something – тело функции, должно помещаться в одну строчку и будет автоматически возвращаться без return
# Take 1 argument and add 3 to it
lambda x: x + 3

Один из примеров использования лямбда-функции – переименование колонок в датафрэйме. Здесь мы делаем их заглавными и заменяем дефисы на нижние подчёркивания

In [165]:
# df is a dataframe as usual
df = df.rename(columns=lambda c: c.upper().replace('-', '_'))

Серии

pd.Series – более примитивный тип данных в pandas, соответствует колонке датафрэйма. В ней хранятся данные одного типа (числа/строки/...). Работая с колонкой, мы работаем именно с серией. Часть методов и аттрибутов серии и датафрэйма совпадают.

Документация

Применение функций к датафрэйму

apply – применяет переданную в него функцию ко всем колонкам вызванного датафрэйма. Чтобы применить функцию к одной колонке датафрэйма, можно выбрать её перед применением apply, например:

In [167]:
df.PICKUP_DT.apply(lambda time: time.split(' ')[0]).head()
Out[167]:
0    2015-01-01
1    2015-01-01
2    2015-01-01
3    2015-01-01
4    2015-01-01
Name: PICKUP_DT, dtype: object
In [169]:
df = pd.read_csv("https://stepik.org/media/attachments/bookings.csv",sep=';')
df['Reservation Status'].apply(lambda status: status.split('-')[-1]).head()
Out[169]:
0    Out
1    Out
2    Out
3    Out
4    Out
Name: Reservation Status, dtype: object

Объединение датафрэймов

Зачастую называется джойном. Очень частая операция, которую можно сделать с помощью нескольких функций. Одна из них – merge. Обязательным аргументом является другой датафрэйм, с которым планируется объединение. Объединение идёт по общей колонке, у которой имеется одинаковый смысл и общие значения в обоих датафрэймах. Существуют различные типы джойнов, они будут рассмотрены в курсе по SQL. Самый частый, пожалуй, inner.

Здесь мы объединяем датафрэйм users_data с датафрэймом users_lovely_brand_data по колонке tc с помощью inner джойна:

In [171]:
user_data = pd.read_csv("https://stepik.org/media/attachments/user_data.csv")
logs = pd.read_csv("https://stepik.org/media/attachments/logs.csv")
user_data.merge(logs, how = 'inner', on = 'client').head()
Out[171]:
client premium age success platform time
0 46346 False 58 True phone 1585452839
1 4391 False 55 False phone 1585409861
2 27372 False 64 False phone 1585446018
3 11989 False 44 True computer 1585403698
4 60664 False 49 True phone 1585406918

В результате получается один датафрэйм, где колонки из 2-ух таблиц, относящиеся к одному наблюдению, объединяются в строку. Звучит сложно, поэтому для практики стоит попробовать сделать несколько простых джойнов :)

Дополнительные аргументы функции merge

  • how – как объединять датафрэймы, одно из inner, outer, left, right
  • on – общая колонка, по которой будет идти объединение

Документация

Индекс и имена колонок

Индекс – это лэйбл строки в таблицы, по умолчанию является её номером. А имена колонок... это имена колонок, то есть лэйблы, по которым мы можем обращаться к каждому из столбцов.

У датафрэйма есть 2 атрибута index и columns, позволяющие получить доступ к соответствующей информации в виде array (на самом деле не совсем array)

In [172]:
df = pd.read_csv('https://docs.google.com/spreadsheets/d/e/2PACX-1vR-ti6Su94955DZ4Tky8EbwifpgZf_dTjpBdiVH0Ukhsq94jZdqoHuUytZsFZKfwpXEUCKRFteJRc9P/pub?gid=889004448&single=true&output=csv')
df.index
Out[172]:
RangeIndex(start=0, stop=607, step=1)
In [173]:
df.columns
Out[173]:
Index(['date', 'time', 'event', 'platform', 'ad_id', 'ad_cost_type',
       'ad_cost'],
      dtype='object')

Сброс индекса

Иногда вам может захотеться перевести индекс датафрэйма в колонку. Для этого существует метод reset_index. Индексом становится дефолтная последовательность чисел от 0 до числа строк - 1.

In [175]:
df.head()
Out[175]:
date time event platform ad_id ad_cost_type ad_cost
0 2019-04-01 2019-04-01 0:00:48 view web 121288 CPM 187.4
1 2019-04-01 2019-04-01 0:04:41 view ios 121288 CPM 187.4
2 2019-04-01 2019-04-01 0:07:50 view android 121288 CPM 187.4
3 2019-04-01 2019-04-01 0:07:50 view android 121288 CPM 187.4
4 2019-04-01 2019-04-01 0:08:46 view ios 121288 CPM 187.4
In [176]:
df.reset_index().head()
Out[176]:
index date time event platform ad_id ad_cost_type ad_cost
0 0 2019-04-01 2019-04-01 0:00:48 view web 121288 CPM 187.4
1 1 2019-04-01 2019-04-01 0:04:41 view ios 121288 CPM 187.4
2 2 2019-04-01 2019-04-01 0:07:50 view android 121288 CPM 187.4
3 3 2019-04-01 2019-04-01 0:07:50 view android 121288 CPM 187.4
4 4 2019-04-01 2019-04-01 0:08:46 view ios 121288 CPM 187.4

Удаление индекса

Аргумент drop отвечает за то, нужно ли переводить индекс в колонку, или убрать его из таблицы:

In [178]:
df.reset_index(drop=True).head()
Out[178]:
date time event platform ad_id ad_cost_type ad_cost
0 2019-04-01 2019-04-01 0:00:48 view web 121288 CPM 187.4
1 2019-04-01 2019-04-01 0:04:41 view ios 121288 CPM 187.4
2 2019-04-01 2019-04-01 0:07:50 view android 121288 CPM 187.4
3 2019-04-01 2019-04-01 0:07:50 view android 121288 CPM 187.4
4 2019-04-01 2019-04-01 0:08:46 view ios 121288 CPM 187.4

Поиск пустых значений

isna - это чудо-метод, с помощью которого можно быстро найти пропущенные значения в датафрэйме

In [179]:
df.head()
Out[179]:
date time event platform ad_id ad_cost_type ad_cost
0 2019-04-01 2019-04-01 0:00:48 view web 121288 CPM 187.4
1 2019-04-01 2019-04-01 0:04:41 view ios 121288 CPM 187.4
2 2019-04-01 2019-04-01 0:07:50 view android 121288 CPM 187.4
3 2019-04-01 2019-04-01 0:07:50 view android 121288 CPM 187.4
4 2019-04-01 2019-04-01 0:08:46 view ios 121288 CPM 187.4

Применив его, на выходе мы получаем датафрэйм той же размерности, где в каждой ячейке True или False в зависимости от того было ли значение пропущено.

In [180]:
df.isna().head()
Out[180]:
date time event platform ad_id ad_cost_type ad_cost
0 False False False False False False False
1 False False False False False False False
2 False False False False False False False
3 False False False False False False False
4 False False False False False False False

В связке с ним можно использовать, например, sum, чтобы посмотреть на число NA в разных колонках.

In [181]:
df.isna().sum()
Out[181]:
date            0
time            0
event           0
platform        0
ad_id           0
ad_cost_type    0
ad_cost         0
dtype: int64

Графики

Графики – важная часть анализа данных, так как они наглядно (если тип графика хорошо подобран) представляют данные и позволяют быстро разобраться в сути.

Чтобы в юпитер ноутбуке отображались графики выполните строчку:

%matplotlib inline

Существуют разные способы создания графиков в python, несколько популярных библиотек:

  • pandas
  • seaborn
  • matplotlib

Давайте начнём постепенно в них разбираться!

pandas

Самый простой способ визуализировать данные – вызвать метод plot у датафрэйма (или его колонки). Например, гистограмма значений в колонке age:

In [182]:
user_data.head()
Out[182]:
client premium age
0 46346 False 58
1 4391 False 55
2 27372 False 64
3 11989 False 44
4 60664 False 49
In [183]:
user_data.age.plot(kind='hist', bins=15)
Out[183]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f52b3983450>

Другой вариант записи:

In [184]:
user_data.age.plot.hist(bins=15)
Out[184]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f52a8193990>

Функции рисования имеют весьма большое количество параметров, используйте их при необходимости. bins здесь – число диапазонов (корзин/бакетов), на которые мы разделяем значения.

Документация

seaborn

Продвинутая библиотека, позволяющая сделать очень красивые графики. Конвенционально загружается как

import seaborn as sns

Далее пример создания нескольких графиков с её помощью.

Гистограмма

In [43]:
import seaborn as sns


# Use link from your course
df = pd.read_csv("https://stepik.org/media/attachments/bookings.csv",sep=';')
In [187]:
sns.distplot(df['Lead Time'])
Out[187]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f529a348cd0>

Документация

Боксплот

In [188]:
sns.boxplot(data=df, x='Hotel', y='Lead Time')
Out[188]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f529bb00ed0>
In [189]:
sns.barplot(data=df, x='Hotel', y='Lead Time')
Out[189]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f52b38aa290>

matplotlib

Базовая библиотека для рисования в питоне. На ней построены более продвинутые и простые в использовании типа seaborn. Через matplotlib можно нарисовать что угодно, но часто на это уходит слишком много строк кода, и её в основном используют для тонкой настройки графиков и их сохранения.

Традиционно matplotlib импортируется следующим образом:

import matplotlib.pyplot as plt

Настройка графиков

Важный момент – большинство настроек должны быть написаны к каждому графику отдельно. Иными словами, настройки, написанные в ячейке с одним графиком, не будут применены к другому.

Изменить размер

В figure в figsize подаётся кортеж (как список, только в круглых скобках) с масштабом графика формата (ширина, высота)

In [44]:
import matplotlib.pyplot as plt


plt.figure(figsize=(9,6))
sns.distplot(df.query('Hotel == "City Hotel"')['Lead Time'], kde = False)
Out[44]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f5b73b59f60>

Больше информации

Сохранение картинки

Сохранить график можно с помощью savefig , где аргумент – путь к сохраняемой картинке (желаемое название и формат):

In [191]:
sns.distplot(df.query('Hotel == "City Hotel"')['Lead Time'], kde = False)
plt.savefig("1.png")

Урок№4

Проверка на начало строки

startswith - строковый метод, принимающий другую строку и возвращающий True или False в зависимости от того, начинается ли исходная строка с переданной.

In [192]:
'Abyss'.startswith('Ab')
Out[192]:
True
In [193]:
'Abyss'.startswith('ab')
Out[193]:
False

Альтернативный способ создания списка

List comprehension - часто используется как лаконичная (и убыстренная) замена циклов, где заполняется список

xs = [i + 3 for i in range(10)]

Аналогично

xs = []
for i in range(10):
    xs.append(i + 3)

В comprehension'е можно прописать условия и даже вложенные циклы. Однако не стоит сильно их нагружать: читаемость кода - одно из его самых важных качеств, а вложенные comprehension'ы делают код сложнее для понимания

# Get all even numbers
evens = [i for i in range(10) if i % 2 == 0]

# Analogous to
even = []
for i in range(10):
    if i % 2 == 0:
        even.append(i)

Больше информации

Конвертация типов в датафрэйме

Нередка ситуация, когда тип данных в колонке не соответствует желаемому (почему это вообще важно? Для разных типов определены разные операции - '0.15' * 100 не переведёт дробь в проценты).

Чтобы это исправить, есть метод astype, в который можно передать словарь, где ключи это названия колонок, а значения - новые типы для них. Метод возвращает новый датафрэйм с изменёнными типами

df = df.astype({'money': 'float'})  # df.money will be rational number after this line

Для конвертации типов колонок есть более простой вариант - передайте желаемый тип при вызове astype от колонки

df.height = df.height.astype('float')  # df.height will be rational number

Документация

Удаление колонок

Чтобы убрать часть колонок из датафрэйма, воспользуйтесь методом drop, куда можно передать список из названий, которые нужно убрать. Метод также позволяет убирать строки по индексу, для указания измерения, в котором мы работаем, используется аргумент axis (0 - строки, 1 - колонки). Лучше использовать более понятные columns/index. Возвращается новый датафрэйм

df = df.drop(columns='Date')  # drop Date column

df = df.drop(index=350)  # drop row with 350 index

Документация

Фильтрация дубликатов

Дубликаты - повторяющиеся наблюдения, которых не должно быть. Быстро их убрать позволяет метод drop_duplicates, возвращающий таблицу без них.

df = df.drop_duplicates()  # df will contain <= rows than before after this operation

Вывести все дубликаты

df.loc[df.duplicated()]

Дополнительные аргументы

subset - принимает список колонок, по которым нужно смотреть дупликацию

# Drop only if duplicates are in 'Date' or 'Last' columns
df.drop_duplicates(subset=['Date', 'Last'])

Документация

Сравнение строк

Строки сравниваются в лексикографическом порядке (по алфавиту, как в языковых словарях).

In [100]:
'1' < '2'
Out[100]:
True
In [45]:
'abc' < 'b'
Out[45]:
True

Соединение сравнений (comparison chaining)

Эти 2 записи тождественны:

In [46]:
1 < 2 and 2 < 3
Out[46]:
True
In [47]:
1 < 2 < 3
Out[47]:
True

Конвертация во время

pd.to_datetime() - метод, позволяющий превратить строки во время. Это позволяет удобно с ним работать.

Аттрибут dt позволяет извлекать временные характеристики из колонки с датой.

In [194]:
df['arrival full date'] = pd.to_datetime(df['arrival full date'])
In [195]:
df['arrival full date'].dt.year.head()
Out[195]:
0    2015
1    2015
2    2015
3    2015
4    2015
Name: arrival full date, dtype: int64
In [196]:
df['arrival full date'].dt.month.head()
Out[196]:
0    7
1    7
2    7
3    7
4    7
Name: arrival full date, dtype: int64

Документация

Предварительный распарс

Также, вы можете заранее распарсить дату при загрузке датасэта, передав в parse_dates список колонок, в которых содержится дата.

pd.read_csv('some.csv', parse_dates=[1])  # order of columns starting from 0

Документация

Открывание файлов

Рассмотренный способ открывания файлов с помощью pd.read_csv - не единственный, и не первый в питоне. Традиционно любой файл открывается с помощью функции open, принимающий путь к файлу. Это менее удобно, так как является базовым способом, который далее усложняется в том же pd.read_csv

file = open('path_to_file')

У file есть различные методы на чтение содержимого, например readlines.

lines = file.readlines()  # lines is a list with lines from the file

В конце работы с файлом (то есть когда он вам больше не понадобится) его нужно закрыть, для чего используется метод … close

file.close()

Существует более удобный и предпочтительный способ с контекстным мэнеджером.

Больше информации

Просмотр содержимого папок

Просмотр папок и многие другие операции, связанные с файлами и папками выполняются с помощью библиотеки os. Для получения списка файлов используется listdir. Метод os.listdir принимает путь к папке и возвращает её содержимое в виде списка.

In [53]:
import os


# Paste your home directory
os.listdir('/home/jupyter-a.ilin/shared/homeworks//python_ds_miniprojects/5_subsid/subsid')
Out[53]:
['prod_activations_logs.csv',
 'tm_sales_1.csv',
 'tm_sales_2.csv',
 'tm_sales_3.csv']

Названия файлов в папке вместе с путём к ней, позволяют реконструировать полный путь и работать с этими файлами.

path = '/etc'
path_to_file = path + '/' + os.listdir(path)[0]  # os.path.join is better for constructing path

При вложенности папок и необходимости добраться до дна можно использовать os.walk

In [58]:
# Paste your home directory
for path, dirs, files in os.walk('/home/jupyter-a.ilin/shared/homeworks/python_ds_miniprojects/5_subsid/subsid'):
    print(files) # возвращает файлы в формате list
['prod_activations_logs.csv', 'tm_sales_1.csv', 'tm_sales_2.csv', 'tm_sales_3.csv']
In [59]:
for path, dirs, files in os.walk('/home/jupyter-a.ilin/shared/homeworks/python_ds_miniprojects/5_subsid/'):
    print(dirs)  # возвращает директории в формате list (если лист пустой значит нет директорий
['subsid']
[]
In [61]:
for path, dirs, files in os.walk('/home/jupyter-a.ilin/shared/homeworks/python_ds_miniprojects/5_subsid/'):
    print(path)  # возвращает пути до директории на каждой итерации цикла
/home/jupyter-a.ilin/shared/homeworks/python_ds_miniprojects/5_subsid/
/home/jupyter-a.ilin/shared/homeworks/python_ds_miniprojects/5_subsid/subsid

На каждой итерации (1 этап цикла) метод возвращает тройку из пути к нынешней папке, списков папок и файлов, хранящихся в этой папке.

Также есть более предпочтительный вариант - модуль pathlib.

Больше информации

Прерывание цикла

При необходимости можно выйти из цикла с помощью слова break. Это бывает нужно сделать при выполнении определённых условий.

Искусственный пример - на итерации, где в i попадёт число больше 5, цикл будет прерван, и будет выполняться код ниже него. Таким образом будут напечатаны все числа до того, которое больше 5 (7 в данном случае).

In [231]:
numbers = [1, 3, 2, 4, 5, 7, 10]

for i in numbers:
    if i > 5:
        break
    print(i)
1
3
2
4
5

Пропуск итераций цикла

Другой частый случай - пропуск каких-то итераций, для этого используется слово continue.

Здесь будут напечатаны только чётные числа.

In [110]:
numbers = [1, 3, 2, 4, 5, 7, 10]

for i in numbers:
    if i % 2 != 0:
        continue
    print(i)
2
4
10

Справедливости ради, простой код может быть написан без применения continue.

In [111]:
for i in numbers:
    if i % 2 == 0:
        print(i)
2
4
10

А он понадобится для более сложных случаев.

Больше информации

Удаление пропущенных значений

dropna - метод, позволяющий выкинуть из датафрэйма все строки, содержащие пропущенные значения.

In [78]:
data = pd.read_csv('https://stepik.org/media/attachments/taxi_peru.csv', sep=';')
data.loc[:, ['driver_id','taxi_id']].head()
Out[78]:
driver_id taxi_id
0 583949a89a9ee17d19e3ca4f137b6b4c b12f4f09c783e29fe0d0ea624530db56
1 NaN NaN
2 NaN NaN
3 NaN NaN
4 d665fb9f75ef5d9cd0fd89479380ba78 0accdd3aa5a322f4129fa20b53278c69

Так, мы выкинем все строки, где было хотя бы одно пропущенное значение.

In [79]:
data.loc[:, ['driver_id','taxi_id']].dropna().head()
Out[79]:
driver_id taxi_id
0 583949a89a9ee17d19e3ca4f137b6b4c b12f4f09c783e29fe0d0ea624530db56
4 d665fb9f75ef5d9cd0fd89479380ba78 0accdd3aa5a322f4129fa20b53278c69
5 baacf396f773709519bbde35a5eab861 baacf396f773709519bbde35a585d91b
6 e1332f68e81526e498e4d845233a6d7d e1332f68e81526e498e4d845235baf80
7 d665fb9f75ef5d9cd0fd89479380ba78 0accdd3aa5a322f4129fa20b53278c69

У dropna есть набор интересных параметров, ознакомиться с которыми можно в документации.

Документация

Проверка на вхождение

В питоне, чтобы узнать, есть ли элемент в списке, используется оператор in.

In [114]:
2 in [2,4,5]
Out[114]:
True

Больше информации

В пандасе есть более эффективный метод isin, принимающий коллекцию, в которой содержатся искомые значения.

Документация

Логическое индексирование

In [80]:
ads_data = pd.read_csv('~/shared/homeworks/python_ds_miniprojects/6/ads_data.csv')
In [81]:
wanted_clients = [34734, 112260]
ads_data.loc[ads_data.client_union_id.isin(wanted_clients)].head()
Out[81]:
date time event platform ad_id client_union_id campaign_union_id ad_cost_type ad_cost has_video target_audience_count
0 2019-04-01 2019-04-01 00:00:48 view android 45061 34734 45061 CPM 200.6 0 1955269
26 2019-04-01 2019-04-01 00:03:28 view android 45061 34734 45061 CPM 200.6 0 1955269
53 2019-04-01 2019-04-01 00:04:58 view ios 45061 34734 45061 CPM 200.6 0 1955269
102 2019-04-01 2019-04-01 00:10:23 view ios 45061 34734 45061 CPM 200.6 0 1955269
192 2019-04-01 2019-04-01 00:15:42 view android 45061 34734 45061 CPM 200.6 0 1955269

Запросы

Согласно документации, работает быстрее.

In [82]:
wanted_clients = [34734, 112260]
ads_data.query('client_union_id == @wanted_clients').head()
Out[82]:
date time event platform ad_id client_union_id campaign_union_id ad_cost_type ad_cost has_video target_audience_count
0 2019-04-01 2019-04-01 00:00:48 view android 45061 34734 45061 CPM 200.6 0 1955269
26 2019-04-01 2019-04-01 00:03:28 view android 45061 34734 45061 CPM 200.6 0 1955269
53 2019-04-01 2019-04-01 00:04:58 view ios 45061 34734 45061 CPM 200.6 0 1955269
102 2019-04-01 2019-04-01 00:10:23 view ios 45061 34734 45061 CPM 200.6 0 1955269
192 2019-04-01 2019-04-01 00:15:42 view android 45061 34734 45061 CPM 200.6 0 1955269

Использование == как аналог isin является старым, и не работает со списками. Вместо него лучше использовать in (наконец-то они его ввели).

In [17]:
wanted_clients = [34734, 112260]
ads_data.query('client_union_id in @wanted_clients').head()
Out[17]:
date time event platform ad_id client_union_id campaign_union_id ad_cost_type ad_cost has_video target_audience_count
0 2019-04-01 2019-04-01 00:00:48 view android 45061 34734 45061 CPM 200.6 0 1955269
26 2019-04-01 2019-04-01 00:03:28 view android 45061 34734 45061 CPM 200.6 0 1955269
53 2019-04-01 2019-04-01 00:04:58 view ios 45061 34734 45061 CPM 200.6 0 1955269
102 2019-04-01 2019-04-01 00:10:23 view ios 45061 34734 45061 CPM 200.6 0 1955269
192 2019-04-01 2019-04-01 00:15:42 view android 45061 34734 45061 CPM 200.6 0 1955269

Соединение датафрэймов

Для объединения 2-ух или более датафрэймов существует метод pd.concat(). Он принимает кортеж из нескольких датафрэймов, и возвращает один большой из них. Соединять можно вертикально (увеличиваем число строк) или горизонтально (увеличиваем число столбцов).

Документация

Добавление строк

Есть менее общий вариант - метод append() принимает другой датафрэйм и прибавляет его вертикально.

Для записывания в датафрэйм новых строк из другого датафрэйма можно использовать метод append, возвращающий новый датафрэйм, где прибавлены строки из 2-ого.

both_df = first_df.append(second_df)

Документация

Урок№5

Открывание сжатых файлов

У функции pd.read_csv есть аргумент compression, который принимает строчку типа компрессии и открывает заархивированный файл.

# Here type of compression is zip
ads_data = pd.read_csv('ads_data.csv.zip', compression='zip')

Больше информации

Формат UNIX time

Время может быть указано в разном формате, один из них - число секунд, прошедших с 1970 года. Кажется странным? (По-моему, да). Зато удобно - времена представляются как целые числа, которые легко вычитать, сравнивать. А при необходимости можно сконвертировать в human-readable формат.

pd.to_datetime(1554076848, unit='s')

Документация

Атрибуты времени

Временные серии обладают атрибутом dt, в котором находится множество атрибутов и методов для доступа ко времени. Давайте посмотрим на часть из них.

In [5]:
data_time = pd.read_csv('https://docs.google.com/spreadsheets/d/e/2PACX-1vR-ti6Su94955DZ4Tky8EbwifpgZf_dTjpBdiVH0Ukhsq94jZdqoHuUytZsFZKfwpXEUCKRFteJRc9P/pub?gid=889004448&single=true&output=csv')
data_time['time'] = pd.to_datetime(data_time['time'])
data_time.time.head()
Out[5]:
0   2019-04-01 00:00:48
1   2019-04-01 00:04:41
2   2019-04-01 00:07:50
3   2019-04-01 00:07:50
4   2019-04-01 00:08:46
Name: time, dtype: datetime64[ns]

Микросекунды

dt.microsecond - сколько микросекунд в указанном времени (то есть, если время 5 минут, 0 секунд и 3 микросекунды, то он вернёт 3, а не $5 \cdot 60 \cdot 10^6$)

In [7]:
data_time.time.dt.microsecond.head()
Out[7]:
0    0
1    0
2    0
3    0
4    0
Name: time, dtype: int64

Секунды

In [8]:
data_time.time.dt.second.head()
Out[8]:
0    48
1    41
2    50
3    50
4    46
Name: time, dtype: int64

Минуты

In [9]:
data_time.time.dt.minute.head()
Out[9]:
0    0
1    4
2    7
3    7
4    8
Name: time, dtype: int64

Часы

In [11]:
data_time.time.dt.hour.head()
Out[11]:
0    0
1    0
2    0
3    0
4    0
Name: time, dtype: int64

День месяца

In [12]:
data_time.time.dt.day.head()
Out[12]:
0    1
1    1
2    1
3    1
4    1
Name: time, dtype: int64

Номер дня недели

In [14]:
data_time.time.dt.weekday.head()
Out[14]:
0    0
1    0
2    0
3    0
4    0
Name: time, dtype: int64

Название дня недели

In [15]:
data_time.time.dt.day_name().head()
Out[15]:
0    Monday
1    Monday
2    Monday
3    Monday
4    Monday
Name: time, dtype: object

Номер недели в году

In [16]:
data_time.time.dt.week.head()
Out[16]:
0    14
1    14
2    14
3    14
4    14
Name: time, dtype: int64

Номер месяца

In [17]:
data_time.time.dt.month.head()
Out[17]:
0    4
1    4
2    4
3    4
4    4
Name: time, dtype: int64

Название месяца

In [18]:
data_time.time.dt.month_name().head()
Out[18]:
0    April
1    April
2    April
3    April
4    April
Name: time, dtype: object

Год

In [19]:
data_time.time.dt.year.head()
Out[19]:
0    2019
1    2019
2    2019
3    2019
4    2019
Name: time, dtype: int64

Число дней в текущем месяце

In [20]:
data_time.time.dt.daysinmonth.head()
Out[20]:
0    30
1    30
2    30
3    30
4    30
Name: time, dtype: int64

Разность времени

Немного о timedelta - это тип данных, соответствующий разнице 2-ух времён, то есть какая-то продолжительность времени.

In [62]:
data = pd.read_csv('https://stepik.org/media/attachments/taxi_peru.csv', 
                   sep=';',
                   parse_dates=['start_at','end_at','arrived_at'])

data['wait_time'] = (data['arrived_at'] - data['start_at'])
data.wait_time.head(10)
Out[62]:
0            00:18:00
1                 NaT
2                 NaT
3                 NaT
4   -1 days +23:55:00
5   -1 days +23:53:00
6   -1 days +23:51:00
7            00:08:00
8            00:17:00
9            00:23:00
Name: wait_time, dtype: timedelta64[ns]

Компоненты

Все единицы измерени времени, можно извлечь сразу с помощью атрибута components.

In [24]:
data.wait_time.dt.components.head(6)
Out[24]:
days hours minutes seconds milliseconds microseconds nanoseconds
0 0.0 0.0 18.0 0.0 0.0 0.0 0.0
1 NaN NaN NaN NaN NaN NaN NaN
2 NaN NaN NaN NaN NaN NaN NaN
3 NaN NaN NaN NaN NaN NaN NaN
4 -1.0 23.0 55.0 0.0 0.0 0.0 0.0
5 -1.0 23.0 53.0 0.0 0.0 0.0 0.0

quantile

Метод для поиска определённых перцентилей. Принимает число от 0 до 1, обозначающее перцентиль в виде доли:

  • 0 - 0-ой перцентиль
  • 0.1 - 10-ый перцентиль
  • 0.75 - 75-ый перцентиль (так же 3-ий квартиль)
In [25]:
ads_data = pd.read_csv('~/shared/homeworks/python_ds_miniprojects/6/ads_data.csv')
ads_data.head()
Out[25]:
date time event platform ad_id client_union_id campaign_union_id ad_cost_type ad_cost has_video target_audience_count
0 2019-04-01 2019-04-01 00:00:48 view android 45061 34734 45061 CPM 200.6 0 1955269
1 2019-04-01 2019-04-01 00:00:48 view web 121288 121288 121288 CPM 187.4 0 232011
2 2019-04-01 2019-04-01 00:01:03 view android 102737 102535 102564 CPC 60.7 0 4410
3 2019-04-01 2019-04-01 00:01:03 view android 107564 106914 107564 CPM 217.3 0 62711
4 2019-04-01 2019-04-01 00:01:09 view android 4922 37 4400 CPC 60.1 0 1183501
In [26]:
ads_data.target_audience_count.quantile(q=0.75)
Out[26]:
2512711.0

Также в q можно передать список всех желаемых перцентилей.

In [27]:
ads_data.target_audience_count.quantile(q=[0.5,0.7])
Out[27]:
0.5      32756.0
0.7    1540082.0
Name: target_audience_count, dtype: float64

Что делать со значениями, не попадающими в перцентиль.

Если ровно по заданному перцентилю в датафрэйме нет значения, то по дефолту метод линейно выведет его. Поменять это поведение можно с помощью параметра interpolation. Вариант 'higher' берёт большую точку из смежных.

In [28]:
ads_data.target_audience_count.quantile(q=[0.5,0.7], interpolation='higher')
Out[28]:
0.5      32756
0.7    1540082
Name: target_audience_count, dtype: int64

P.S.: Краткость - сестра таланта, но не в создании названий параметров в одну букву. Здесь сложно перепутать, так как название метода намекает, но когда будете создавать свои функции и методы, называйте всё осмысленно.

Документация

Сводные таблицы

Сводные таблицы - удобный способ преобразовать данные, с возможностью применения к ним агрегирующей функции. В pandas есть 2 функции, различающиеся только тем проводится ли агрегация.

Обе принимают 3 аргумента:

  • index - название колонки, значения из которой станут индексами
  • columns - название колонки, значения из которой станут колонками
  • values - название колонки, значения из которой распределяться по сформированным группам

Теперь сами функции.

pivot

Преобразует датафрэйм в таблицу, где значения использованных колонок становятся новыми индексами и колонками.

In [30]:
ads_data = pd.read_csv('~/shared/homeworks/python_ds_miniprojects/6/ads_data.csv')
ads_data = ads_data\
    .groupby(['date','platform'], as_index=False)\
    .agg({'target_audience_count': 'sum'})
ads_data.head()
Out[30]:
date platform target_audience_count
0 2019-04-01 android 6638604471
1 2019-04-01 ios 4313789707
2 2019-04-01 web 3118932012
3 2019-04-02 android 141255148481
4 2019-04-02 ios 85843032660
In [31]:
ads_data.pivot(index='date', columns='platform', values='target_audience_count')
Out[31]:
platform android ios web
date
2019-04-01 6638604471 4313789707 3118932012
2019-04-02 141255148481 85843032660 58909038606
2019-04-03 179525904185 106262403621 71607301620
2019-04-04 3217891210293 1927505726214 1284981258382
2019-04-05 647052797669 391796011202 257014889562
2019-04-06 5890894323 3482494402 2403487043

Нужно принимать во внимание, что если в таблице, к которой мы хотим применить pivot нет каких то значений, например, данных по android в день 2019-04-01, то в результирующей pivot таблице мы получим NaN.

Сейчас мы специально удалим из ads_data первую строку в которой данные как раз по андроиду за 2019-04-01 и применим к ней pivot.

In [32]:
ads_data.iloc[1:].pivot(index='date', columns='platform', values='target_audience_count')
Out[32]:
platform android ios web
date
2019-04-01 NaN 4.313790e+09 3.118932e+09
2019-04-02 1.412551e+11 8.584303e+10 5.890904e+10
2019-04-03 1.795259e+11 1.062624e+11 7.160730e+10
2019-04-04 3.217891e+12 1.927506e+12 1.284981e+12
2019-04-05 6.470528e+11 3.917960e+11 2.570149e+11
2019-04-06 5.890894e+09 3.482494e+09 2.403487e+09

pivot_table

Всё как в предыдущем методе, только можно произвести агрегацию, получая одно значение из группы с одинаковыми значениями в новых индексах и колонках. По умолчанию берётся среднее от группы значений.

In [33]:
ads_data.pivot_table('target_audience_count', index='date', columns='platform')
Out[33]:
platform android ios web
date
2019-04-01 6638604471 4313789707 3118932012
2019-04-02 141255148481 85843032660 58909038606
2019-04-03 179525904185 106262403621 71607301620
2019-04-04 3217891210293 1927505726214 1284981258382
2019-04-05 647052797669 391796011202 257014889562
2019-04-06 5890894323 3482494402 2403487043
In [34]:
ads_data.pivot_table('target_audience_count', index='date', columns='platform', aggfunc='max')
Out[34]:
platform android ios web
date
2019-04-01 6638604471 4313789707 3118932012
2019-04-02 141255148481 85843032660 58909038606
2019-04-03 179525904185 106262403621 71607301620
2019-04-04 3217891210293 1927505726214 1284981258382
2019-04-05 647052797669 391796011202 257014889562
2019-04-06 5890894323 3482494402 2403487043

Альтернативный способ создания колонок

Колонки в датафрэйме можно также создать с помощью метода assign. Он возвращает исходный датафрэйм с добавленными колонками – нужно перезадать переменную, чтобы изменить датафрэйм.

В метод передаются аргументы формата название колонки = её содержимое – как название параметра и его значение при вызове функции. Здесь название колонок нужно писать без кавычек.

Так, мы задаём новую колонку rel, значения в которой являются отношением значений в колонке target_audience_count к значениям в колонке ad_cost:

In [64]:
ads_data = pd.read_csv('~/shared/homeworks/python_ds_miniprojects/6/ads_data.csv')
ads_data = ads_data.assign(rel = ads_data.target_audience_count / ads_data.ad_cost)

Замена пропущенных значений

Очень важная вещь в некоторых сферах. Одно из простых решений - заменить все на одно значение (например, 0). Это можно сделать с помощью метода fillna, принимающего значение, на которое будут заменены пропущенные значения.

In [47]:
data = pd.read_csv('https://stepik.org/media/attachments/taxi_peru.csv', sep=';')
data.loc[:, ['driver_id','taxi_id']].head(3)
Out[47]:
driver_id taxi_id
0 583949a89a9ee17d19e3ca4f137b6b4c b12f4f09c783e29fe0d0ea624530db56
1 NaN NaN
2 NaN NaN
In [48]:
data.loc[:, ['driver_id','taxi_id']].head(3).fillna(0)
Out[48]:
driver_id taxi_id
0 583949a89a9ee17d19e3ca4f137b6b4c b12f4f09c783e29fe0d0ea624530db56
1 0 0
2 0 0

loc

До этого при отборе определённых строк мы пользовались методом query или использовали []. Вместе с тем существует метод loc (и его собрат iloc), позволяющий выбрать поднабор строк и колонок из датафрэйма. В некоторых случаях loc удобнее, но обычно запись с ним более громоздкая и он работает медленнее query.

In [50]:
ads_data = pd.read_csv('~/shared/homeworks/python_ds_miniprojects/6/ads_data.csv')
ads_data.head(5)
Out[50]:
date time event platform ad_id client_union_id campaign_union_id ad_cost_type ad_cost has_video target_audience_count
0 2019-04-01 2019-04-01 00:00:48 view android 45061 34734 45061 CPM 200.6 0 1955269
1 2019-04-01 2019-04-01 00:00:48 view web 121288 121288 121288 CPM 187.4 0 232011
2 2019-04-01 2019-04-01 00:01:03 view android 102737 102535 102564 CPC 60.7 0 4410
3 2019-04-01 2019-04-01 00:01:03 view android 107564 106914 107564 CPM 217.3 0 62711
4 2019-04-01 2019-04-01 00:01:09 view android 4922 37 4400 CPC 60.1 0 1183501

Эта запись отберёт все строки из датафрэйма, где значения в колонке platform равны 'web', и колонки от time до client_union_id

In [51]:
ads_data.loc[ads_data['platform'] == 'web', 'time':'client_union_id'].head()
Out[51]:
time event platform ad_id client_union_id
1 2019-04-01 00:00:48 view web 121288 121288
5 2019-04-01 00:01:09 view web 10325 107
9 2019-04-01 00:01:19 view web 16589 747
12 2019-04-01 00:01:37 view web 45259 35576
13 2019-04-01 00:01:37 view web 108707 108678

Кусок df['platform'] == 'web' возвращает логическую серию, где напротив нужных значений стоит True. Вместо df['platform'] == 'web' может быть любое выражение, которое даст коллекцию True/False размером с число строк в датафрэйме.

Lineplot

Линии, что тут ещё сказать. По оси x и y откладываются значения точек, эти точки соединяются. Аргумент hue принимает имя колонки, по значениям которой идёт разделение на цвета.

In [65]:
sns.lineplot(x='date', y='target_audience_count', hue='event', data=ads_data)
Out[65]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f5b70fd8cc0>

Heatmap

Удобный тип графика, когда есть множество значений с 2-мя категориальными признаками (обычно это индекс и колонки в датафрэйме). По осям откладываются значения этих категориальных переменных, каждая ячейка - значение, которое мы визуализируем. Интенсивность ячейки пропорциональна значению.

In [55]:
df_num = pd.DataFrame(data={'col1': [5, 7, 8, 2, 9, 1], 'col2': [3, 4, 2, 7, 2, 3],
                            'col3': [2, 6, 7, 5, 8, 1], 'col4': [1, 9, 3, 6, 7, 2]})
sns.heatmap(df_num)
Out[55]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f113fa35d30>

Регулярные выражения (регэкспы/ре)

При работе с текстовыми данными часто возникает необходимость их парсить (то есть извлекать нужные данные из всего текста). Давайте представим небольшой пример, у нас есть данные о почтовых адресах пользователях, и мы хотим узнать, с каких доменов (всё, что после @) у нас пользователей больше.

vasya@yandex.ru
katya_ivanova@gmail.com
sasha@karpov.courses.com
masha@gmail.com

Мы могли бы посчитать по доменным именам value_counts, если бы они были у нас в колонке в датафрэйме. Но, что делать, если нам даны целые мэйлы?

На помощь приходят регулярные выражения. Регулярные выражения - это специальный язык для описания низкого уровня языковой грамматики. Не углубляясь в определения - ре позволяют вычленить из регулярного текста (его структура одинакова/почти одинакова на протяжении всего текста) нужные нам части

Для начала, разберём всё в простом питоне, а потом уже в пандасе. В данном примере с почтой мы можем просто воспользоваться строковые методы питона -

  • засплитить по @
  • взять последнюю часть получившегося списка
  • ...
  • PROFIT

Но не на всех задачах встроенные методы так хорошо работают. Сначала посмотрим, как решить этот таск ре, а потом разберём что-нибудь посложнее. Итак решение, и его объяснение:

In [46]:
import re


mail = 'vasya@yandex.ru'

pattern = re.compile('@([\w.]+)')

pattern.findall(mail)
Out[46]:
['yandex.ru']
  • import re - импотиртируем библиотеку для работы с регулярными выражениями, в чистом питоне их нет

  • pattern = re.compile('@([\w.]+)') - с помощью функции compile из модуля re создаём паттерн (образец), который будем искать в тексте, и помещаем его в переменную pattern. Паттерн создаётся при помощи строки - о том, с чем совпадает (что матчит) этот паттерн мы поговорим дальше. Паттерн обладает набором методов (так же, как у датафрэйма есть методы), один из которых мы и используем

  • pattern.findall(mail) - применяем метод findall на строке с почтой. Метод findall возвращает список со всеми встречаниями паттерна (pattern) в строке, где мы ищем (mail)
    В результате мы получили список с одним мэтчем - ['yandex.ru']

На первый взгляд кажется непонятно (и неудивительно - мы ещё не обсуждали как описывается паттерн) и бессмысленно, ведь есть split. Однако, у этого способа уже на чуть усложнённой задаче есть плюсы.

In [47]:
text = '''We have several emails - vasya@yandex.ru, katya_ivanova@gmail.com,
sasha@karpov.courses.com and also masha@gmail.com'''

pattern.findall(text)
Out[47]:
['yandex.ru', 'gmail.com', 'karpov.courses.com', 'gmail.com']

Одним питоновским сплитом мы бы тут не отделались! В следующей главе поговорим об описании паттерна.

Регэксповая песочница

Азбука регулярных выражений

Как уже говорилось, ре это язык о регулярном языке. Для его описания используется набор символов, рассмотрим наиболее частые из них.

Буквы и цифры

Все буквы и цифры в паттерне обозначают буквы. То есть если мы напишем:

In [48]:
import re


text = 'the gray fox jumps over the lazy dog'

pattern = re.compile('ox')
pattern.findall(text)
Out[48]:
['ox']

То мы найдём все ox в тексте. С числами такая же история. Да и со многими знаками типа @.

Метасимволы

Специальные символы для ре, обозначающие группу значений

  • \d - любая цифра (digits)
  • \D - всё что угодно кроме цифры
  • \s - любой пробельный символ (spaces)
  • \S - всё что угодно кроме пробельного символа
  • \w - любая буква, цифра или _ (words)
  • \W - всё что угодно кроме буквы, цифр или _

То есть нижний регистр - хотим это, верхний регистр - хотим не это Ещё есть

  • . - любой символ

Пара примеров

Тройки цифр:

In [49]:
text = '+7-921-000-00-00 +7-981-555-55-55'

pattern = re.compile('\d\d\d')
pattern.findall(text)
Out[49]:
['921', '000', '981', '555']

Фрагменты из 4-ёх знаков, начинающиеся с в:

In [50]:
asimov = '''Робот не может причинить вред человеку или своим бездействием допустить, чтобы человеку был причинён вред.
Робот должен повиноваться всем приказам, которые даёт человек, кроме тех случаев, когда эти приказы противоречат Первому Закону.
Робот должен заботиться о своей безопасности в той мере, в которой это не противоречит Первому или Второму Законам.'''


pattern = re.compile('в...')
pattern.findall(asimov)
Out[50]:
['вред',
 'веку',
 'воим',
 'вием',
 'веку',
 'вред',
 'вино',
 'вать',
 'всем',
 'век,',
 'в, к',
 'воре',
 'вому',
 'воей',
 'в то',
 'в ко',
 'воре',
 'вому']

Группы

Скобочки () имеют особое значение - они обозначают группы символов в паттерне. Благодаря этому мы можем извлечь кусочки из заматчившегося паттерна. Например, достанем только код (города? оператора? в общем какой-то код) из телефонного номера:

In [51]:
text = '+7-921-000-00-00 +7-981-555-55-55'

pattern = re.compile('(\d\d\d)-(\d\d\d)')
pattern.findall(text)
Out[51]:
[('921', '000'), ('981', '555')]

Обратите внимание, что мы получаем кортежи, где каждый элемент - группа из одного матча. Это позволяет нам извлечь нужную группу из каждого матча (хотя бы просто циклом по pattern.findall(...) с извлечением 0-ого элемента). В то время как раньше, мы получали все тройки цифр сплошняком. Другое наблюдение - минус в паттерне никак не отображается - мы матчим в тексте 3 цифры, минус, 3 цифры, то есть он должен быть в тексте, чтобы заматчить, но мы можем убрать его из аутпута.

Квантификаторы

Это просто ... в общем это очень круто) Квантификаторы - это символы, позволяющие специфицировать, сколько раз нужно повторить то, что идёт до них. Вот их виды:

  • * - сколько угодно раз (0 - бесконечность)
  • + - 1 или больше раз
  • ? - 0 или 1 раз (то есть или предыдущий символ будет, или нет)
  • {} - в скобочках можно указать точное время или диапазон, читайте подробнее о них и других символах в документации

Квантификаторы можно ставить после символа или группы. К примеру, отберём весь текст, начинающийся со слов человек где есть:

In [52]:
asimov = '''Робот не может причинить вред человеку или своим бездействием допустить, чтобы человеку был причинён вред.
Робот должен повиноваться всем приказам, которые даёт человек, кроме тех случаев, когда эти приказы противоречат Первому Закону.
Робот должен заботиться о своей безопасности в той мере, в которой это не противоречит Первому или Второму Законам.'''


pattern = re.compile('человек.*')
pattern.findall(asimov)
Out[52]:
['человеку или своим бездействием допустить, чтобы человеку был причинён вред.',
 'человек, кроме тех случаев, когда эти приказы противоречат Первому Закону.']

* и + стараются сожрать как можно больше символов в паттерн (почти как Уроборос).

Эскапирование (экранирование)

Что делать, если хочется искать \d (то есть идущие друг за другом \ и d) или просто \? Заэкранировать их ещё одним \! Однако, стоит помнить, что в питоне \ тоже специальный символ, поэтому придётся добавлять ещё один \ и в результате паттерн будет выглядеть захламлённым. Чтобы этого не происходило, используйте raw строки, то есть ставьте буковку r перед строкой с паттерном.

Разумеется, это далеко не всё, но этого хватит, чтобы начать)

Регэкспы как дота просты для базового освоения, и сложны для использования на уровне мастера, но при этом бывают очень полезны в рутине. Но сразу предостерегаем вас - если есть готовая библиотека для парсинга специфичного текста, то воспользуйтесь ею (html - beautiful soap, json - json), так как регэксп это в большинстве случаев решение, которое подходит, чтобы быстро решить задачку с текстом без определённого формата или с простым форматом (регулярным).

Документация

Строковые методы пандаса

Для строковых колонок датафрэйма есть специальный атрибут str, содержащий множество методов работы со строками (по сути там векторизованные питоновские методы для строк). Вызов самого по себе str ничего особо не даёт.

In [75]:
df = pd.DataFrame({'name': ['Aristotle', 'Zenon', 'Kant', 'Hume', 'Heidegger']})
df
Out[75]:
name
0 Aristotle
1 Zenon
2 Kant
3 Hume
4 Heidegger

Применение

Строковые методы из str применяются к каждой ячейке колонки. Например, проверим, начинаются ли значения колонки name на A

In [76]:
df['name'].str.startswith('A')
Out[76]:
0     True
1    False
2    False
3    False
4    False
Name: name, dtype: bool

О том, что произошло - после обращения к атрибуту str мы вызвали метод startswith, в который передали строку A. Это аналогично вызову типа:

In [77]:
'Aristotle'.startswith('A')
Out[77]:
True

Только мы делаем это со всей колонкой. И как результат получаем такую же колонку булиновских значений - True, если начинается на a, и False, если не начинается. Исходная колонка не меняется, возвращается новая.

Или, перевод всех значений в upper case:

In [78]:
df['name'].str.upper().head()
Out[78]:
0    ARISTOTLE
1        ZENON
2         KANT
3         HUME
4    HEIDEGGER
Name: name, dtype: object

Слайсинг

По сути, когда мы пишем str, то получаем доступ к строкам в колонке, и как бы вызываем от них строковый метод. Вдобавок к этому, мы можем делать срезы, возьмём первые 5 букв, к примеру:

In [83]:
df['name'].str[:5].head()
Out[83]:
0    Arist
1    Zenon
2     Kant
3     Hume
4    Heide
Name: name, dtype: object

Что аналогично:

In [82]:
'Aristotle'[:5]
Out[82]:
'Arist'

Сплит строк

По аналогии со сплитом обычной строки, мы можем засплиттить строки в серии, и получить на каждую ячейку по списку. Наш датафрэйм немного изменился:

In [84]:
df = pd.DataFrame({'info': ['Aristotle, (384 BC-322BC)', 
                            'Zeno of Elea, (c. 495 BC-c. 430 BC)', 
                            'Immanuel Kant, (1724-1804)', 
                            'David Hume, (1711-1776)', 
                            'Martin Heidegger, (1889-1976)']})
df
Out[84]:
info
0 Aristotle, (384 BC-322BC)
1 Zeno of Elea, (c. 495 BC-c. 430 BC)
2 Immanuel Kant, (1724-1804)
3 David Hume, (1711-1776)
4 Martin Heidegger, (1889-1976)
In [89]:
df['info'].str.split(',')
Out[89]:
0              [Aristotle,  (384 BC-322BC)]
1    [Zeno of Elea,  (c. 495 BC-c. 430 BC)]
2             [Immanuel Kant,  (1724-1804)]
3                [David Hume,  (1711-1776)]
4          [Martin Heidegger,  (1889-1976)]
Name: info, dtype: object

Здесь мы получили списки с двумя элементами.

Колонки со списками

При работе с такими колонками, по спискам можно индексироваться и слайситься так же при помощи атрибута str

In [90]:
df['info'].str.split(',').str[0]
Out[90]:
0           Aristotle
1        Zeno of Elea
2       Immanuel Kant
3          David Hume
4    Martin Heidegger
Name: info, dtype: object

Что при работе с одним списком аналогично:

In [91]:
['David Hume', ' (1711–1776)'][0]
Out[91]:
'David Hume'

Парсинг строковых колонок в пандасе

Чтобы извлечь данные из строк в пандасе есть специальный метод - extract. Он принимает паттерн ре, позволяющий выдрать нужные куски из текста в отдельные колонки.

In [92]:
df['info']
Out[92]:
0              Aristotle, (384 BC-322BC)
1    Zeno of Elea, (c. 495 BC-c. 430 BC)
2             Immanuel Kant, (1724-1804)
3                David Hume, (1711-1776)
4          Martin Heidegger, (1889-1976)
Name: info, dtype: object

Извлечём отсюда информацию об имени и даты жизни

In [95]:
df['info'].str.extract('(?P<name>\w+), \((?P<data>.+)\)')
Out[95]:
name data
0 Aristotle 384 BC-322BC
1 Elea c. 495 BC-c. 430 BC
2 Kant 1724-1804
3 Hume 1711-1776
4 Heidegger 1889-1976

Итак

  • df['info'].str - обращаемся к атрибуту со строковыми методами
  • extract - вызываем метод, выдирающий части текста
  • (?P<name>\w+) - это именованная группа, она как группа, только к ней можно обращаться по имени
    • (?P...) - говорит питону, что это именованная группа
    • <name> - имя группы, в данном случае name
    • \w+ - матчим буквы/цифры/подчёркивания, которые встречают 1 или больше раз подряд
  • , \( - запятая, пробел и скобочка, которые идут после первой группы (\ потому что символ скобки имеет специальное значение в ре)
  • (?P<data>.+) - другая именованная группа
    • ?P - опять же, это идентификатор группы
    • <data> - имя группы data
    • .+ - берём любой символ 1 или больше раз подряд
  • \) - скобочка после 2-ой группы

Найдя в ячейке текст, подходящий под такое описание, extract выдерет его, разобьёт на указанные группы, и поместит в новые колонки с именами как в указанных группах. Данный паттерн не самый оптимальный, но не использует новых метасимволов.

extract возвращает новый датафрэйм с экстрагированным текстом.

Документация

Отбор колонок по названию

В пандасе есть удобный метод отбора колонок или строк по их названию - filter. Помимо строк он так же может работать с регэкспами, что позволяет гибко отбирать колонки.

pd.DataFrame.filter(items/like/regex, axis)
  • items - принимает список с названиями колонок или строк, особой разницы по сравнению с loc'ом нет
  • like - принимает строку, и возвращает все колонки, где в названии содержится строка, переданная в like
  • regex - принимает строку, означающую паттерн ре, возвращает все колонки с названиями, матчимящимися на паттерн
  • axis - параметр для обозначения того, отбираем мы колонки или строки, принимает 'columns' или 'index', по умолчанию фильтрует колонки

Посмотрим на примере данных о перевозках

Отберём все колонки с id в названии:

In [96]:
ads_data.filter(like='id').head()
Out[96]:
ad_id client_union_id campaign_union_id has_video
0 45061 34734 45061 0
1 121288 121288 121288 0
2 102737 102535 102564 0
3 107564 106914 107564 0
4 4922 37 4400 0

Как видите, мы получили только колонки с id в названии - все колонки с идентификаторами и has_video

А теперь возьмём по паттерну только колонки с идентификаторами:

In [97]:
ads_data.filter(regex='_id').head()
Out[97]:
ad_id client_union_id campaign_union_id
0 45061 34734 45061
1 121288 121288 121288
2 102737 102535 102564
3 107564 106914 107564
4 4922 37 4400

Урок№6

NumPy

Модуль для работы с данными (преимущественно численными), на котором основан пандас. Часть методов у этих модулей пересекается, и функции нампая хорошо работают с сериями и датафрэймами. Традиционно сокращается при импорте как np

import numpy as np

Для примера возьмём 10-ичный логарифм от серии:

In [98]:
import numpy as np


s = pd.Series({'b': 10, 'a': 100, 'c': 1000})
s
Out[98]:
b      10
a     100
c    1000
dtype: int64
In [99]:
np.log10(s)
Out[99]:
b    1.0
a    2.0
c    3.0
dtype: float64

То есть достаточно просто передать внутрь функции объект, к элементам которого мы хотим применить функцию

Документация

Оконные функции

Иногда (часто при работе с временными данными) нужно произвести агрегирующие вычисления, захватывающие определённый промежуток данных (не всю колонку). То есть мы будем работать в определённом "окне" значений колонки. Окна бывают разные, простой вариант - скользящее -

Для вычисления 1-ого значения берётся, допустим 4, предыдущих значения и то, которое рассчитываем. Для вычисления 2-ого берутся значения со сдвигом на 1 (то есть со 2-ого по 6-ое) и так далее.

In [81]:
df_num
Out[81]:
col1 col2 col3 col4
0 5 3 2 1
1 7 4 6 9
2 8 2 7 3
3 2 7 5 6
4 9 2 8 7
5 1 3 1 2
In [82]:
df_num.col1.rolling(3).mean()
Out[82]:
0         NaN
1         NaN
2    6.666667
3    5.666667
4    6.333333
5    4.000000
Name: col1, dtype: float64

Скользящее окно призывается с помощью метода rolling, принимающего период окна. Эта функция похожа на группировку тем, что результат не просматривается, и необходимо применить агрегирующую функцию (в примере выше mean).

4 первых значения не были вычислены, так как для них нет 5 значений, по которым они бы вычислялись.

Избавление от NaN'ов.

Если рассчёт каждого нового значения ровно по периоду не важен, можно использовать параметр min_periods, принимающий число, указывающее сколько минимум должно быть значений, чтобы посчитать результат.

In [83]:
df_num.col1.rolling(3, min_periods=1).mean()
Out[83]:
0    5.000000
1    6.000000
2    6.666667
3    5.666667
4    6.333333
5    4.000000
Name: col1, dtype: float64

Скользящее среднее

Посмотрим на скользящее среднее более подробно. Периодически требуется не только преобразовать данные, но и сгладить сам ряд. Сглаживание временных рядов позволяет избавиться от шума в данных и более точно увидеть линию общего тренда. Один из способов — использование скользящего среднего. Что же это такое?

В pandas есть уже готовая реализация этого метода — pd.rolling(). Функция принимает несколько параметров, первый и самый важный из которых window — размер окна, также называемый шириной окна. Данный параметр отвечает за число наблюдений, которые используются для подсчета скользящего среднего. К примеру,

$$SMA_{t} = \frac{1}{n}\sum_{i=0}^{n-1}x_{t-i}\frac{x_t + x_{t-1} + \dots + x_{t-(n-1)}}{n}$$

где n — размер окна, t — момент времени.

Предположим, имеется 5 наблюдений:

In [84]:
df = pd.DataFrame({'value': [0, 1, 2, 3, 4]})
df
Out[84]:
value
0 0
1 1
2 2
3 3
4 4

Для подсчета скользящего среднего с размером окна 2 вычисления будут выглядеть следующим образом:

$$SMA_{t} = \frac{x_t - x_{t-1}}{2}$$

где $x_t$ — значение в текущий момент времени, $x_{t-1}$ — в предыдущий.

In [85]:
df.rolling(window=2).mean()
Out[85]:
value
0 NaN
1 0.5
2 1.5
3 2.5
4 3.5

Обратите внимание, что теперь на месте самого первого значения стоит NaN, поскольку указанный размер окна 2 подразумевает наличие двух значений для подсчета, и вполне логично, что скользящее среднее для первого наблюдения вычислить не получится, поскольку других значений перед ним нет. Если увеличить размер окна до 3, то NaN будет стоять и вместо второго наблюдения, и так далее.

Еще один вариант использования pd.rolling() — центрированное скользящее среднее. В таком случае используются наблюдения до, после и во время t. В pandas за это отвечает параметр center, который по умолчанию равен False.

Для подсчета центрированного среднего с размером окна 3:

$$SMA_{t} = \frac{x_{t-1} + x_t + x_{t+1}}{3}$$

где $x_t$ — значение в текущий момент времени, $x_{t-1}$ — в предыдущий, $x_{t+1}$ — последующий.

In [86]:
df.rolling(window=3, center=True).mean()
# (0+1+2)/3=1
# (1+2+3)/3=2
Out[86]:
value
0 NaN
1 1.0
2 2.0
3 3.0
4 NaN

Экспоненциальное сглаживание

Следующий шаг — экспоненциальное сглаживание. В предыдущем подходе (скользящее среднее), использовались nnn последних наблюдений и все они имели равный вес. В данном случае веса экспоненциально уменьшаются, т.е. более "старые" события имеют меньший вес, а для подсчета используются все имеющиеся наблюдения.

$$EMA_t = \alpha * P_t + (1 - \alpha)*EMA_{t-1}$$

где:

  • $\alpha$ — веса от 0 до 1; чем выше, тем больший вес имеют новые значения и меньше старые,
  • $P_t$ — значение в момент времени $t$,
  • $EMA_{t−1}$ — значение скользящего среднего в момент ${t-1}$.

Более подробно про расчет EMA можно почитать здесь.

Для подсчета экспоненциального скользящего среднего в pandas есть метод ewm(). Например,

In [87]:
df
Out[87]:
value
0 0
1 1
2 2
3 3
4 4
In [88]:
df.ewm(span=2).mean()
Out[88]:
value
0 0.000000
1 0.750000
2 1.615385
3 2.550000
4 3.520661

Итерация по нескольким спискам одновременно

Время от времени вам будет нужно пройтись по нескольким спискам одновременно - то есть получать элементы с одним индексом из каждого из них. Можно сделать это так:

In [89]:
nums = [3, 5, 7]
letters = ['all', 'for', 'exoplanets colonization']

for i in range(len(nums)):
    num = nums[i]
    letter = letters[i]
    # do something with them

Но есть более удобный способ - функция zip()! По сути она позволяет перебирать элементы списков одновременно, возвращая на каждой итерации кортеж с элементами из одинаковой позиции соответствующих списков.

In [90]:
nums = [3, 5, 7]
letters = ['all', 'for', 'exoplanets colonization']

for i in zip(nums, letters):
    num = i[0]
    letter = i[1]
    # do something with them

Кажется, что лучше не стало, и в такой записи, и вправду стало ненамного лучше. Но мы можем сразу извлечь num и letter из кортежа с помощью записи.

In [91]:
nums = [3, 5, 7]
letters = ['all', 'for', 'exoplanets colonization']

for num, letter in zip(nums, letters):
    # do something with them

Связано это с тем, что в питоне можно распаковывать кортежи и списки в переменные, например.

In [92]:
num, letter = (3, 'all')  # num == 3 and letter == 'all'

Здесь мы присваиваем элементы из кортежа (3, 'all') в соответствующие переменные.

Больше информации

Общая настройка графика

Многие настройки рисования можно установить 1 раз для скрипта (обычно в его начале), избавившись таким образом от повторов.

In [93]:
# Font size will be increased, background of figures will be white, grid will be present and size of plots will be increased

sns.set(
    font_scale=2,
    style="whitegrid",
    rc={'figure.figsize':(20,7)}
        )

Кастомизация графика

Хорошие графики обладают подписанными осями, заголовком и начинаются от 0 при сравнении между собой нескольких значений (например, на барплоте)!

Есть несколько способов провести кастомизацию графика.

Через объект графика

При создании графика возвращается объект, который хранит о нём информацию. Через него можно задать параметры кастомизации.

In [94]:
ax = df.plot()  # create plot

ax.set_xlabel('X axis name')  # Label of x axis
ax.set_ylabel('Y axis name')  # Label of y axis
ax.set_title('Plot title')  # Title of the plot

y_labels = [str(int(i * 100)) + '%' for i in ax.get_yticks()]  # Prepare custom labels of axis values

ax.set_yticklabels(y_labels)  # Set new labels
ax.set_xticklabels(labels=df.index, rotation=90)  # Same just for x axis and totate labels 
#to perpendicular configuration

sns.despine()  # Get rid of axis on the plot

Через методы matplotlib

In [99]:
import matplotlib.pyplot as plt
ax = df.plot()
plt.xlabel('X axis name')
plt.ylabel('Y axis name')
plt.title('Plot title')

y_labels = [str(int(i * 100)) + '%' for i in ax.get_yticks()]

ax.set_yticklabels(y_labels)
ax.set_xticklabels(labels=df.index, rotation=90)

sns.despine()

Рецепт для построения графика

Давайте подробнее посмотрим на форматирование графиков, а именно на их расположени, подписи осей, рамки и сетки Как построить несколько графиков рядом друг с другом?

Для этого можно использовать plt.subplots(). Благодаря этому методу мы разобьём общее полотно графика на части и получим матрицу, где в каждой ячейке можно размесить свой график. Функция принимает несколько параметров:

  • nrows – число рядов/строк, на которые мы разделим полотно
  • ncols – число колонок, на которые мы разделим полотно
  • figsize – размер картинки, общего полотна
  • sharey – будет ли ось y общей для графиков (пошаренной) и между какими (можно задать общую ось между всеми графиками, между графиками одной строки или одного столбца)
  • sharex – то же самое для оси x
In [100]:
import matplotlib.dates as mdates


fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(20, 10), sharey='col', sharex=True)

Мы указали 2 строки и 2 колонки для графиков (то есть у нас будет матрица 2 на 2, где можно уместить до 4-ёх рисунков). Размер картинки указывается как кортеж с шириной и высотой графика 'col' в sharey указывает на то, что у графиков в каждой колонке будет одинаковая ось y True в sharex означает, что ось x будет одинаковая у всех графиков

В результате возвращается 2 объекта - сама картинка и оси каждого из графиков (plt довольно навороченная библиотека в плане организации графика), которые мы сразу записываем в переменные fig и axes.

В последнем находится массив из объектов axes:

In [101]:
np.array([['<matplotlib.axes._subplots.AxesSubplot object at 0x11d35dd50>',
        '<matplotlib.axes._subplots.AxesSubplot object at 0x11e71df10>'],
       ['<matplotlib.axes._subplots.AxesSubplot object at 0x11e783410>',
        '<matplotlib.axes._subplots.AxesSubplot object at 0x11e7bc310>']],
      dtype=object)
Out[101]:
array([['<matplotlib.axes._subplots.AxesSubplot object at 0x11d35dd50>',
        '<matplotlib.axes._subplots.AxesSubplot object at 0x11e71df10>'],
       ['<matplotlib.axes._subplots.AxesSubplot object at 0x11e783410>',
        '<matplotlib.axes._subplots.AxesSubplot object at 0x11e7bc310>']],
      dtype=object)

Это по сути numpy эррей 2 на 2 - то есть матрица по-человечески. В каждой её ячейке можно нарисовать график независимый от других ячеек (или зависимый по осям, если их зашарить).

Рисуем

Для отрисовки графиков используем цикл. Сначала соединяем с помощью zip():

  • значения для параметра window
  • оси axes
  • список цветов для каждого из графиков
In [102]:
avocado_mean = pd.read_csv('https://stepik.org/media/attachments/avocado_mean.csv')
avocado_mean.head()
Out[102]:
Date AveragePrice
0 2015-01-04 1.301296
1 2015-01-11 1.370648
2 2015-01-18 1.391111
3 2015-01-25 1.397130
4 2015-02-01 1.247037
In [103]:
windows = [2, 20, 30, 40]
colors = ['coral', 'blue', 'green', 'purple']

for window, ax, color in zip(windows, axes.flatten(), colors): 
    ax.plot(avocado_mean.rolling(window=window).mean(), label=window, color=color)

Здесь мы используем метод .flatten() у axes, чтобы перевести матрицу в одномерный массив и просто по нему проитерироваться. Содержимое axes.flatten():

In [104]:
np.array(['<matplotlib.axes._subplots.AxesSubplot object at 0x11d35dd50>',
       '<matplotlib.axes._subplots.AxesSubplot object at 0x11e71df10>',
       '<matplotlib.axes._subplots.AxesSubplot object at 0x11e783410>',
       '<matplotlib.axes._subplots.AxesSubplot object at 0x11e7bc310>'],
      dtype=object)
Out[104]:
array(['<matplotlib.axes._subplots.AxesSubplot object at 0x11d35dd50>',
       '<matplotlib.axes._subplots.AxesSubplot object at 0x11e71df10>',
       '<matplotlib.axes._subplots.AxesSubplot object at 0x11e783410>',
       '<matplotlib.axes._subplots.AxesSubplot object at 0x11e7bc310>'],
      dtype=object)

Форматируем

Теперь для каждого графика:

In [105]:
for ax in axes.flatten():
    # удаляем рамку
    ax.set_frame_on(False)  
    # устанавливаем major locator – 4 января для каждого года
    ax.xaxis.set_major_locator(mdates.MonthLocator(bymonth=1, bymonthday=4))  
    # показывать в формате сокращенного названия месяца и дня (Jan 04)
    ax.xaxis.set_major_formatter(mdates.DateFormatter('%b %d'))
    # под major locator - minor locator, т.е. редактируем minor ticks
    ax.xaxis.set_minor_locator(mdates.YearLocator(month=1, day=1))
    # показываем год
    ax.xaxis.set_minor_formatter(mdates.DateFormatter('\n%Y'))
    # делаем сетку графика совсем немного серой и наполовину прозрачной
    ax.grid(True, color='#e2e2e2', alpha=0.5)

В результате мы определим где будут находиться лэйблы тиков и как они будут выглядеть.

Добавляем названия

Для каждого рисунка устанавливаем название с помощью ax.set() и форматирования строк. Также используем ax.tick_params() для изменения внешнего вида ticks.

In [106]:
for name, ax in zip(['1','2','3','4'], axes.flatten()):
    ax.set(title='График {}'.format(name))
    ax.tick_params(labelbottom=True, which='both')

Здесь мы проитерировались по нашим графикам, задали заголовок каждого (title), а также указали, что хотим нарисовать названия у тиков снизу.

Строим 📊

Объединяем кусочки кода в одну ячейку, plt.show() и готово!

In [107]:
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(20, 10), sharey='col', sharex=True)

windows = [2, 20, 30, 40]
colors = ['coral', 'blue', 'green', 'purple']

for window, ax, color in zip(windows, axes.flatten(), colors): 
    ax.plot(avocado_mean.rolling(window=window).mean(), label=window, color=color)

np.array(['<matplotlib.axes._subplots.AxesSubplot object at 0x11d35dd50>',
       '<matplotlib.axes._subplots.AxesSubplot object at 0x11e71df10>',
       '<matplotlib.axes._subplots.AxesSubplot object at 0x11e783410>',
       '<matplotlib.axes._subplots.AxesSubplot object at 0x11e7bc310>'],
      dtype=object)

for ax in axes.flatten():
    # удаляем рамку
    ax.set_frame_on(False)  
    # устанавливаем major locator – 4 января для каждого года
    ax.xaxis.set_major_locator(mdates.MonthLocator(bymonth=1, bymonthday=4))  
    # показывать в формате сокращенного названия месяца и дня (Jan 04)
    ax.xaxis.set_major_formatter(mdates.DateFormatter('%b %d'))
    # под major locator - minor locator, т.е. редактируем minor ticks
    ax.xaxis.set_minor_locator(mdates.YearLocator(month=1, day=1))
    # показываем год
    ax.xaxis.set_minor_formatter(mdates.DateFormatter('\n%Y'))
    # делаем сетку графика совсем немного серой и наполовину прозрачной
    ax.grid(True, color='#e2e2e2', alpha=0.5)

for name, ax in zip(['1','2','3','4'], axes.flatten()):
    ax.set(title='График {}'.format(name))
    ax.tick_params(labelbottom=True, which='both')
plt.show()

Дефолтные аргументы функций

Если какой-то из аргументов вашей функции часто имеет одно значение, имеет смысл прописать его по умолчанию. Для этого поставьте после имени аргумента = и укажите значение, которое будет принимать аргумент, если он не получит его при вызове функции. Аргументы по умолчанию должны идти после обычных.

In [108]:
def greetings(name='guys'):
    print('Hi,', name)
In [109]:
greetings('Bob')
Hi, Bob
In [110]:
greetings()
Hi, guys

Объединение таблиц по нескольким полям

Если указать в параметре on функции pd.merge список из колонок, то произойдёт объединение по их комбинации. То есть будут объединены строки, где совпадают значения во всех указанных в on колонках.

# Merge by combination of Company Id and Company Name
orders_with_sales_team = pd.merge(order_leads, sales_team, on=['Company Id','Company Name'])

Документация

Преобразование континуальной переменной в категориальную

Если вам нужно перейти от чисел к категориям, воспользуйтесь функцией pd.cut. Она принимает массив значений и число интервалов/список из границ интервалов.

In [101]:
values = pd.Series([1, 2, 3, 4, 5, 6])
values
Out[101]:
0    1
1    2
2    3
3    4
4    5
5    6
dtype: int64
In [104]:
pd.cut(values, 3)
Out[104]:
0    (0.995, 2.667]
1    (0.995, 2.667]
2    (2.667, 4.333]
3    (2.667, 4.333]
4      (4.333, 6.0]
5      (4.333, 6.0]
dtype: category
Categories (3, interval[float64]): [(0.995, 2.667] < (2.667, 4.333] < (4.333, 6.0]]
In [103]:
pd.cut(values.head(),[0, 3, 5, 10])
Out[103]:
0    (0, 3]
1    (0, 3]
2    (0, 3]
3    (3, 5]
4    (3, 5]
dtype: category
Categories (3, interval[int64]): [(0, 3] < (3, 5] < (5, 10]]

Как видите, числа заменились на интервалы.

Изменение названий

Для добавления своих названий используется аргумент labels, куда подаётся список из названий интервалов.

In [105]:
pd.cut(values, 
       bins=[0, 3, 5, 10], 
       labels=['low', 'medium', 'high'])
Out[105]:
0       low
1       low
2       low
3    medium
4    medium
5      high
dtype: category
Categories (3, object): [low < medium < high]

distplot

Графики, отражающие распределение значений:

In [3]:
df = pd.read_csv('https://stepik.org/media/attachments/2_taxi_nyc.csv')
df.head()
Out[3]:
pickup_dt pickup_month borough pickups hday spd vsb temp dewp slp pcp 01 pcp 06 pcp 24 sd
0 2015-01-01 01:00:00 Jan Bronx 152 Y 5.0 10.0 30.0 7.0 1023.5 0.0 0.0 0.0 0.0
1 2015-01-01 01:00:00 Jan Brooklyn 1519 Y 5.0 10.0 30.0 7.0 1023.5 0.0 0.0 0.0 0.0
2 2015-01-01 01:00:00 Jan EWR 0 Y 5.0 10.0 30.0 7.0 1023.5 0.0 0.0 0.0 0.0
3 2015-01-01 01:00:00 Jan Manhattan 5258 Y 5.0 10.0 30.0 7.0 1023.5 0.0 0.0 0.0 0.0
4 2015-01-01 01:00:00 Jan Queens 405 Y 5.0 10.0 30.0 7.0 1023.5 0.0 0.0 0.0 0.0
In [116]:
sns.distplot(df.pickups, kde=False)
Out[116]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f26a612c860>

kde

Kernel Density Estimation - аргумент добавляет аппроксимацию распределения, по умолчанию True. При этом распределение шкалируется и становится графиком probability density function.

In [117]:
sns.distplot(df.pickups)
Out[117]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f26a4c8dc18>

plotly

Библиотека для создания интерактивных графиков. Например:

In [5]:
import plotly.express as px


px.line(df, df.pickup_dt[:1000], df.pickups[:1000])

Модули

Немного теории: модульность - важное свойство программ, которое обеспечивается языком программирования. Мы можем разбить код программы по нескольким обособленным по смыслу файлам. Это значительно облегчает разработку и тестирование сколько-нибудь сложных приложений.

Небольшой пример - вы написали с 10-ок функций, решающих ваши задачи. Держать их в одном файле и там же вызывать так себе идея, потому что получается бардак. Хорошим шагом будет разделение функций от скриптов, где вы непосредственно применяете их.

Реализация

Каждый питоновский скрипт (с расширением py, не юпитер ноутбук) является модулем. Чтобы получить доступ к его содержимому необходимо произвести импорт. При импортировании файла он целиком исполняется (как если бы вы выполнили его в юпитер ноутбуке).

Для получения своего модуля:

  • создайте файл
  • напишите там код, который хотите (стандартный вариант - функцию)
  • сохраните файл с английским названием, чтобы он начинался с буквы и не содержал пробелов, в конце .py

Расположение модулей

Чтобы модуль можно было импортировать, он должен быть виден питону. Он смотрит модули в нескольких местах.

  • питоновская папка, куда устанавливаются библиотеки - находится в глубинах питона;
  • папка, откуда запущен питон - просто рабочая папка, где вы работаете с ноутбуком;
  • кастомные места, прописанные в sys.path - любое место, которое вы запишите.

Самый простой вариант - держать ваш файл-модуль в той же папке где работаете.

Немного видов импорта

Помимо импорта видов:

import pandas as pd

и

import os

Существуют другие варианты:

  • from my_module import my_function - импортировать функцию my_function, содержащуюся в my_module.py
    После этого её можно использовать в коде как my_function
  • from my_module import *
    Краткая запись для импорта всех имён из модуля, my_function так же можно использовать сразу в коде по её имени. Все остальные переменные/функции тоже были выгружены из модуля. Это не рекомендуемая практика при импорте большинства модулей, так как захламляется именами из модуля скрипт (меньше свободных имён для ваших переменных). Но вполне валидно для модулей с небольшим числом имён.

Больше информации

Урок№7

Замена элементов в зависимости от их значений

Функция np.where() позволяет задать новые значения, основываясь на старых. Она принимает 3 аргумента -

  • condition – условие, то есть серия со списком True и False
  • x – на что заменить True
  • y – на что заменить False
In [111]:
a = pd.Series([0, 1, 2, 3, 1, 2, 3, 4, 5, 6])
np.where(a > 2, 'Higher than 2', 'Lesser than 2')
Out[111]:
array(['Lesser than 2', 'Lesser than 2', 'Lesser than 2', 'Higher than 2',
       'Lesser than 2', 'Lesser than 2', 'Higher than 2', 'Higher than 2',
       'Higher than 2', 'Higher than 2'], dtype='<U13')

Проверка на непропущенные значения

notna – это метод-антоним isna, возвращает True, если значение не NA. Альтернативный способ получить такой результат – инвертировать результат применения isna с помощью ~ (так во многих языках обозначают not, в логических сериях пандаса так же).

df.notna()

# Same as previous
~df.isna()

Документация

Работа с ошибками

Помните ошибки, которые возникают при работе в питоне? Хорошая новость – их можно обрабатывать, но, естественно, нужно делать это разумно. Что имеется в виду: мы можем сделать так, чтобы программа продолжила работать дальше после ошибки. Это делается с помощью конструкции try-except:

In [120]:
expenditures = 0
income = 100

try:
    ratio = income / expenditures
except:
    print('Something went wrong, mb expenditures are 0?')
Something went wrong, mb expenditures are 0?

try и except должны быть вместе вплотную. Как это работает: после try ставится : и идёт блок кода, который пытается выполниться. Если ему это удаётся, то блок except (тоже с :) пропускается. Если же в блоке try произошла ошибка, то вместо прекращения ошибки идёт переход в блок except и выполняется код, содержащийся там. Далее следует выход из except и программа работает с кодом в скрипте дальше.

Теперь о том, зачем это нужно. Не нужно вставлять try, чтобы ваш код не падал с ошибками, и радоваться. Это специальный инструмент для работы с чувствительными местами, где может произойти ошибка, и где вам нужно действовать разными способами в случаях успешной работы блока или ошибки.

Во многих случаях try-except можно заменить полотном предварительных проверок, то есть проверками выполнимости перед выполнением рискованной операции (в примере выше – проверкой на равенство expenditures 0).

Больше информации

Булиновские результаты логических серий

Мы уже много работали с логическими сериями, например:

In [121]:
bool_ex = pd.Series([True, False, True, True, False],
                    index = ['company1', 'company2', 'company3', 'company4', 'company5'])
bool_ex
Out[121]:
company1     True
company2    False
company3     True
company4     True
company5    False
dtype: bool

Иногда необходимо выяснить агрегированное значение серии – все ли там значения True, или есть ли хотя бы один True среди них. Для этого используются специальные методы.

all

Все ли значения в серии True? Всё равно что поставить and между всеми значениями.

In [122]:
bool_ex.all()
Out[122]:
False

any

Есть ли в серии хотя бы одно значение True? Всё равно что поставить or между всеми значениями.

In [123]:
bool_ex.any()
Out[123]:
True

random

Служит для генерации случайных чисел. Есть аналог в numpy.

In [112]:
import random


# returns number from [a, b]
random.randint(1, 6)
Out[112]:
3

API (Application Programming Interface)

Значительно облегчающая выполнение задач вещь. По сути библиотека от создателей веб-сервиса (сайта, где можно что-то сделать), позволяющая быстро выполнить действия с этим сервисом. Как правило для работы с api необходимо получить токен.

Больше информации

Токен

Токен – это уникальная последовательность символов, позволяющая авторизоваться на сайте и работать с API. Пример токена: d9b70b356593da15f73083d7a0e0554586ca5f743fc0f30dabb993f9917b4317725d4db40a3d5e3729607

vk

API для ВКонтакте, позволяет программно выполнять действия, например, писать сообщения, выбирать друзей и так далее.

Документация по API VK

Подготовка автоматизации:

  • Создать группу
  • В управлении группой зайти в Работу с API
  • Создать ключ
  • Зайти в раздел Сообщения
  • Выбрать Сообщения Сообщества: Включены
  • Зайти в группу
  • Выбрать разрешить сообщения (находится в Ещё, если там стоит Запретить сообщения, то всё в порядке)
  • В меню пригласить группу в чат, нажав Добавить в беседу

Если вам не помогли предыдущие пункты, то вот альтернативный способ:

  • Создать группу
  • В управлении группой зайти в Работу с API
  • Создать ключ
  • Зайти в раздел Сообщения
  • Выбрать Сообщения Сообщества: Включены
  • Зайти в группу
  • Выбрать разрешить сообщения (находится в Ещё, если там стоит Запретить сообщения, то всё в порядке)
  • Зайти в управление
  • Выбрать беседы
  • Создать беседу
  • Нажать на неё
  • Нажать присоединиться

Питоновская часть

Подготовка

import vk_api


# Token which you obtained via vk
app_token = 'token`

# id of the 1st chat
chat_id = 1

# id of my user-receiver
my_id = 148915653

# Initialize session
vk_session = vk_api.VkApi(token=app_token)

# Make it possible to use vk api methods as python methods
vk = vk_session.get_api()

#### Отправка сообщений

vk.messages.send(
    chat_id=chat_id,
    random_id=random.randint(1, 2 ** 31),
    message='Это я, Почтальон Печкин!')

#### Отправка документов

# Specify path to the file and its future name in the message
path_to_file = '/home/arleg/Downloads/Telegram Desktop/corr_plot.pdf'
file_name = 'plot.pdf'

upload_url = vk.docs.getMessagesUploadServer(peer_id=my_id)["upload_url"]
file = {'file': (file_name, open(path_to_file, 'rb'))}

# Send request to post this doc on vk.com
response = requests.post(upload_url, files=file)

json_data = json.loads(response.text)

saved_file = vk.docs.save(file=json_data['file'], title=file_name)
attachment = 'doc{}_{}'.format(saved_file['doc']['owner_id'], saved_file['doc']['id'])

vk.messages.send(
    chat_id=chat_id,  # id of chat where to send
    random_id=random.randint(1, 2 ** 31),  # random number for message identification
    message='Привёз посылку для вашего мальчика!',  # message text, optional here
    attachment=attachment)  # attachment name

Заменив chat_id=chat_id на user_id=my_id, можно отправлять сообщения себе. Только поставьте в my_id свой id)

Документация

Получение доступа к API Google

  1. Перейдите на Google Developer Console
  2. Нажмите на googleapi

Нажмите New project

Введите имя и нажмите Create

  1. Нажмите Enable Apps and Services

  1. Выберите из предложенных Google Drive API и Google Sheets API (они находятся ниже в списке)

Нажмите Enable для каждого апи (нужно для их подключения)

Для выбора следующего API выберите Google APIs слева, а затем перейти в Library

  1. Снова нажмите Google APIs слева и перейдите во вкладку Credentials

  1. Нажмите Create credentials и выберите Service account

  1. Введите имя и нажмите Create

  1. Выберите роль Owner и нажмите Continue

  1. В Credentials теперь появился ваш сервисный аккаунт. Нажмите на три точки справа, а затем Create key
  1. Выберите json и нажмите Create

  1. Скачайте ключ в виде json
  2. Нажмите Done
  3. Откройте файл ключа в текстовом просмотрщике и скопируйте почту в разделе client_email (это действие можно выполнить через библиотеку json из питона)

  1. Для проверки, что всё работает, откройте какую-нибудь таблицу в google docs и выберите там настройки доступа, введите скопированную почту в поле и нажмите готово
  2. Затем в питоне выполните следующее
import gspread
from oauth2client.service_account import ServiceAccountCredentials`


# Specify path to your file with credentials
path_to_credential = 'credentials.json'

# Specify name of table in google sheets
table_name = 'name of your table'

scope = ['https://spreadsheets.google.com/feeds',
         'https://www.googleapis.com/auth/drive']

credentials = ServiceAccountCredentials.from_json_keyfile_name(path_to_credential, scope)

gs = gspread.authorize(credentials)
work_sheet = gs.open(table_name)

# Select 1st sheet
sheet1 = work_sheet.sheet1

# Get data in python lists format
data = sheet1.get_all_values()

# Get header from data
headers = data.pop(0)

# Create df
df = pd.DataFrame(data, columns=headers)
df.head()

После этого в df у вас должно быть содержимое таблицы

Инструкция на английском и ещё одна

Работа с гугл документами

Подготовка

import pandas as pd
import gspread
from df2gspread import df2gspread as d2g
from oauth2client.service_account import ServiceAccountCredentials


scope = ['https://spreadsheets.google.com/feeds',
         'https://www.googleapis.com/auth/drive']


my_mail = 'your@mail'
path_to_credentials = 'crdentials.json'


# Authorization
credentials = ServiceAccountCredentials.from_json_keyfile_name(path_to_credentials, scope)
gs = gspread.authorize(credentials)

#### Загрузка таблицы из гугл доков

# Name of the table in google sheets,
# can be url for open_by_url
# or id (key) part for open_by_key
table_name = 'table name'  # Your table

# Get this table
work_sheet = gs.open(table_name)

# Select 1st sheet
sheet1 = work_sheet.sheet1

# Get data in python lists format
data = sheet1.get_all_values()

# Get header from data
headers = data.pop(0)

# Create df
df = pd.DataFrame(data, columns=headers)
df.head()

#### Создание своей таблицы

# Create empty table
table_name = 'A new spreadsheet'

sheet = gs.create(table_name)


# Make it visible to other guys
sheet.share(my_mail, perm_type='user', role='writer')

Документация

Экспорт датафрэйма в гугл доки

Для переноса датафрэйма в таблицу гугл дока, нужно, чтобы вы сделали эту таблицу из питона. Поэтому прогоните перед этой частью предыдущий раздел с желаемым названием таблицы.

# Create your df
df = ...

# Looks like spreadsheet should be already present at the dist (so, run code in create table section)
sheet = 'Master'
d2g.upload(df, table_name, sheet, credentials=credentials, row_names=True)

При этом необходимо, чтобы в df не было повторяющихся индексов (reset_index)

Документация

json

Библиотека для работы с json'ом – одним из самых распространённых форматов данных в вэбе. Он используется для пересылки данных между веб-сервисами, и очень похож на словари в python. Одноимённая библиотека json используется для его преобразования в действительно питоновский словарь.

import json


# Convert json to python dict
json_data = json.loads(some_json)

Формат
Документация

Интернет-запросы

Библиотека requests позволяет взаимодействовать с сайтами. Метод get() принимает ссылку на сайт в виде строки, и возвращает объект, содержащий ответ с сайта в виде строки.

import requests


query = requests.get(url)

Документация

Конструирование ссылок

Если вам не нравится вручную делать ссылки-запросы для Яндекс.Метрики, есть решение:

In [118]:
from urllib.parse import urlencode


# Base path to service
base_url = 'https://api-metrika.yandex.net/stat/v1/data?'

# Parameters of query
params = {'metrics': 'ym:s:visits',
          'dimensions': 'ym:s:date',
          'id': '44147844'}

visits_url = base_url + urlencode(params)
visits_url
Out[118]:
'https://api-metrika.yandex.net/stat/v1/data?metrics=ym%3As%3Avisits&dimensions=ym%3As%3Adate&id=44147844'

Получившаяся ссылка в нескольких местах не совпадает с полученной ранее, но она такая же – просто в ней произведено дополнительное экранирование символов. Далее её можно точно так же использовать в requests.get

Чтобы закодировать несколько значений параметра с одним именем, просто передайте в словарик вместо значения к нужному параметру список из них, и укажите параметр doseq=True

In [119]:
# Base path to service
base_url = 'https://api-metrika.yandex.net/stat/v1/data?'

# Parameters of query
params = {'metrics': 'ym:s:visits',
          'dimensions': ['ym:s:date', 'ym:s:isRobot'],
          'id': '44147844'}
                
visits_url = base_url + urlencode(params, doseq=True)
visits_url
Out[119]:
'https://api-metrika.yandex.net/stat/v1/data?metrics=ym%3As%3Avisits&dimensions=ym%3As%3Adate&dimensions=ym%3As%3AisRobot&id=44147844'

Параметры requests.get

На самом деле всё можно сделать кратче - достаточно запихнуть словарь с параметрами в params внутри requests.get

requests.get(base_url,
             params={
             'metrics': 'ym:s:visits',
             'dimensions': ['ym:s:date', 'ym:s:isRobot'],
             'id': 44147844
             })

Этот код делает то же самое, что предварительное конструирование ссылки и requests.get на ней

Больше информации

Yandex metrica

Список доступных параметров для запросов в Яндекс.Метрике

import pandas as pd
import requests
import json


# Base url to service
url = 'https://api-metrika.yandex.net/stat/v1/data?'

# &-separated parameters of query in a form of name=value, taken from the metrica site
visits = 'metrics=ym:s:visits&dimensions=ym:s:date&id=44147844'
url = url + visits

# Get json of response
query = requests.get(url)
json_data = json.loads(query.text)

# Conversion of obtained json to dataframe
visits_data = pd.DataFrame([(
                             i['dimensions'][0]['name'],
                             i['metrics'][0]) for i in json_data['data']], 
                           columns=['date', 'visits'])

Телеграм(м)

Маленькое напоминание: Telegram забанен, поэтому для открытия ссылок, связанных с ним, необходим VPN.

Получение токена

Чтобы автоматизировать работу в телеграме необходимо создать бота и получить токен. Для этого:

  • В телеграме найдите @Botfather
  • Нажмите start (или напишите /start) в диалоговом окне - появится сообщение с информацией о создании ботов
  • Oтправьте ему сообщение /newbot
  • Введите имя для бота
  • Введите username бота, он должен заканчиваться на bot
  • Появится сообщение, содержащее токен

Перечень команд доступных для общения с Отцом всех ботов используйте VPN, если ссылка не открывается).

Диалог с ботом

Воспользуйтесь username бота (или ссылкой на него), полученным от Botfather, начните диалог и отправьте ему что-нибудь. Затем введите в браузере ссылку вида:

https://api.telegram.org/bot<token>/getUpdates

Где вместо <token> будет ваш токен.

В открывшемся окне вы увидите содержание json файла, где будет содержаться id чата (result > 0 > chat > id) Сохраните его.

После этого через бота можно посылать сообщения вам с помощью модуля request. Чтобы отправлять сообщения кому-нибудь другому, попросите его начать диалог с ботом, и повторите операцию с просмотром страницы, чтобы выяснить id чата.

Прокси

Для обхода блокировки в коде используются прокси, помните, что их работа, к сожалению, нестабильна – может быть долгий отклик и программа будет долго выполняться, или они могут отрубать доступ из-за большого числа запросов. В таких случаях нужно брать другое прокси. Для Telegram нужны https прокси.

Отправка сообщений

import requests
import json
from urllib.parse import urlencode


token = 'your_token'
chat_id = 123  # your chat id

message = 'test'  # text which you want to send

params = {'chat_id': chat_id, 'text': message}

base_url = f'https://api.telegram.org/bot{token}/'
url = base_url + 'sendMessage?' + urlencode(params)
proxy = {'https': 'https://ip:port'}

resp = requests.get(url, proxies=proxy)

После этого бот должен прислать вам сообщение test

Отправка документов

# Path to necessary file
filepath = 'your_path'

url = base_url + 'sendDocument?' + urlencode(params)

files = {'document': open(filepath, 'rb')}

resp = requests.get(url, files=files, proxies={'https': 'https://ip:port'})

В результате ваш документ отправится в чат.

Другой вариант работы с телеграмом.

Существуют библиотеки для создания ботов в телеге. Лучше пользоваться ими, но будьте осторожны, может слететь юпитер (старых версий, сейчас скорее всего всё будет норм).

Список прокси
Альтернативные инструкции